概述
- Gomock 是一个 Go 语言的 mocking framework,由 go 官方维护
- 可以与 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)
}
}
- 这个测试用例有 2 个目的,一是使用
ctrl.Finish()
断言DB.Get()
被是否被调用,如果没有被调用,后续的 mock 就失去了意义; - 二是测试方法
GetFromDB()
的逻辑是否正确 (如果DB.Get()
返回 error,那么GetFromDB()
返回 -1)。 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"))
Eq(value)
表示与 value 等价的值Any()
可以用来表示任意的入参。Not(value)
用来表示非 value 以外的值。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")
})
Return
返回确定的值Do
Mock 方法被调用时,要执行的操作吗,忽略返回值。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")
}
Times()
断言 Mock 方法被调用的次数MaxTimes()
最大次数MinTimes()
最小次数,少于这个次数将会不会通过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 对象了。