概述

  1. Gomock 是一个 Go 语言的 mocking framework,由 go 官方维护
  2. 可以与 Go 内置的 testing package 协同工作。

安装

首先安装 gomock 的包

go get github.com/golang/mock/gomock

然后安装 gogen 工具,可以自动生成接口的 mock 代码。理论上来说,不用这个工具是可以的,自己用手写。

go install github.com/golang/mock/mockgen@v1.6.0

Mockgen 会安装到 gopath 的 bin 目录下。验证是否安装成功,在命令行输入 mockgen.exe (windows 系统)

快速入门

代码结构如下:

goMock
    |--db.go  
    |--db_mock.go // generated by mockgen  
    |--db_test.go

其中 db.go 的内容如下

package goMock

//go:generate mockgen -source=db.go -destination=./db_mock.go -package=goMock
type DB interface {
   Get(key string) (int, error)
}

// GetFromDB 根据key从数据库获取值
func GetFromDB(db DB, key string) int {
   if value, err := db.Get(key); err == nil {
      return value
    }

   return -1
}

假设 DB 是代码中负责与数据库交互的部分 (在这里用 map 模拟),测试用例中不能创建真实的数据库连接。这个时候,如果我们需要测试 GetFromDB 这个函数内部的逻辑,就需要 mock 接口 DB

第一步:使用 mockgen 生成 db_mock.go。本教程直接使用 go:generate 生成即可。生成的 db_mock.go 如下所示

// Code generated by MockGen. DO NOT EDIT.
// Source: db.go

// Package mocks is a generated GoMock package.
package goMock

import (
 reflect "reflect"

 gomock "github.com/golang/mock/gomock"
)

// MockDB is a mock of DB interface.
type MockDB struct {
   ctrl     *gomock.Controller
 recorder *MockDBMockRecorder
}

// MockDBMockRecorder is the mock recorder for MockDB.
type MockDBMockRecorder struct {
 mock *MockDB
}

// NewMockDB creates a new mock instance.
func NewMockDB(ctrl *gomock.Controller) *MockDB {
 mock := &MockDB{ctrl: ctrl}
 mock.recorder = &MockDBMockRecorder{mock}
   return mock
}

// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockDB) EXPECT() *MockDBMockRecorder {
 return m.recorder
}

// Get mocks base method.
func (m *MockDB) Get(key string) (int, error) {
  m.ctrl.T.Helper()
   ret := m.ctrl.Call(m, "Get", key)
 ret0, _ := ret[0].(int)
 ret1, _ := ret[1].(error)
   return ret0, ret1
}

// Get indicates an expected call of Get.
func (mr *MockDBMockRecorder) Get(key interface{}) *gomock.Call {
    mr.mock.ctrl.T.Helper()
 return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockDB)(nil).Get), key)
}

第二步:新建 db_test.go,写测试用例。

package goMock

import (
    "errors"
  "github.com/golang/mock/gomock"
   "testing"
)

func TestGetFromDB(t *testing.T) {
  ctrl := gomock.NewController(t) // 创建一个控制对象
 defer ctrl.Finish() // 断言 DB.Get() 方法是否被调用

  m := NewMockDB(ctrl) // 控制对象传给mock构造函数
  m.EXPECT().Get(gomock.Eq("Tom")).Return(100, errors.New("not exist"))

   if v := GetFromDB(m, "Tom"); v != -1 {
        t.Fatal("expected -1, but got", v)
    }
}
  1. 这个测试用例有 2 个目的,一是使用 ctrl.Finish() 断言 DB.Get() 被是否被调用,如果没有被调用,后续的 mock 就失去了意义;
  2. 二是测试方法 GetFromDB() 的逻辑是否正确 (如果 DB.Get() 返回 error,那么 GetFromDB() 返回 -1)。
  3. NewMockDB() 的定义在 db_mock.go中,由 mockgen 自动生成。

执行结果

=== RUN   TestGetFromDB
--- PASS: TestGetFromDB (0.00s)
PASS

打桩

在上面的例子中,当 Get() 的参数为 Tom,则返回 error,这称之为 打桩(stub),有明确的参数和返回值是最简单打桩方式。除此之外,检测调用次数、调用顺序,动态设置返回值等方式也经常使用。

参数 (Eq, Any, Not, Nil)

m.EXPECT().Get(gomock.Eq("Tom")).Return(0, errors.New("not exist"))  
m.EXPECT().Get(gomock.Any()).Return(630, nil)  
m.EXPECT().Get(gomock.Not("Sam")).Return(0, nil)   
m.EXPECT().Get(gomock.Nil()).Return(0, errors.New("nil"))
  1. Eq(value) 表示与 value 等价的值
  2. Any() 可以用来表示任意的入参。
  3. Not(value) 用来表示非 value 以外的值。
  4. Nil() 表示 None 值

返回值 (Return, DoAndReturn)

m.EXPECT().Get(gomock.Not("Sam")).Return(0, nil)  
m.EXPECT().Get(gomock.Any()).Do(func(key string) {  
    t.Log(key)  
})  
m.EXPECT().Get(gomock.Any()).DoAndReturn(func(key string) (int, error) {  
    if key == "Sam" {  
        return 630, nil  
    }  
    return 0, errors.New("not exist")  
})
  1. Return 返回确定的值
  2. Do Mock 方法被调用时,要执行的操作吗,忽略返回值。
  3. DoAndReturn 可以动态地控制返回值。

调用次数 (Times)

func TestGetFromDB(t *testing.T) {  
    ctrl := gomock.NewController(t)  
   defer ctrl.Finish()  

 m := NewMockDB(ctrl)  
  m.EXPECT().Get(gomock.Not("Sam")).Return(0, nil).Times(2)  
   GetFromDB(m, "ABC")  
 GetFromDB(m, "DEF")  
}
  1. Times() 断言 Mock 方法被调用的次数
  2. MaxTimes() 最大次数
  3. MinTimes() 最小次数,少于这个次数将会不会通过
  4. AnyTimes() 任意次数(包括 0 次), 有了这个的话,好像 defer ctrl.Finish() 将无作用了

调用顺序 (InOrder)

func TestGetFromDB(t *testing.T) {  
 ctrl := gomock.NewController(t)  
   defer ctrl.Finish() // 断言 DB.Get() 方法是否被调用  

  m := NewMockDB(ctrl)  
  o1 := m.EXPECT().Get(gomock.Eq("Tom")).Return(0, errors.New("not exist"))  
 o2 := m.EXPECT().Get(gomock.Eq("Sam")).Return(630, nil)  
 gomock.InOrder(o1, o2)  
    GetFromDB(m, "Tom")  
 GetFromDB(m, "Sam")  
}

如何编写可 mock 的代码

写可测试的代码与写好测试用例是同等重要的,如何写可 mock 的代码呢?

  • mock 作用的是接口,因此将依赖抽象为接口,而不是直接依赖具体的类。
  • 不直接依赖的实例,而是使用依赖注入降低耦合性。

在软件工程中,依赖注入的意思为,给予调用方它所需要的事物。 “依赖”是指可被方法调用的事物。依赖注入形式下,调用方不再直接指使用“依赖”,取而代之是“注入” 。“注入”是指将“依赖”传递给调用方的过程。在“注入”之后,调用方才会调用该“依赖”。传递依赖给调用方,而不是让让调用方直接获得依赖,这个是该设计的根本需求。
– 依赖注入 - Wikipedia

如果 GetFromDB() 方法长这个样子

func GetFromDB(key string) int {  
   db := NewDB()  
 if value, err := db.Get(key); err == nil {  
        return value  
  }  
 return -1  
}  

对 DB 接口的 mock 并不能作用于 GetFromDB() 内部,这样写是没办法进行测试的。那如果将接口 db DB 通过参数传递到 GetFromDB(),那么就可以轻而易举地传入 Mock 对象了。

最后修改:2023 年 12 月 30 日
如果觉得我的文章对你有用,请随意赞赏