GoMock 是由 Golang 官方开发维护的测试框架,实现了较为完整的基于 interface 的 Mock 功能,能够与
Golang 内置的 testing 包良好集成,也能用于其它的测试环境中。GoMock 测试框架包含了 GoMock 包和
mockgen 工具两部分,其中 GoMock 包完成对桩对象生命周期的管理,mockgen 工具用来生成 interface 对应
的 Mock 类源文件,用来辅助生成测试代码。
一般情况下,我们可以使用 Go Test 单元测试进行测试,但是当待测试的函数/对象的依赖关系很复杂,并且有些
依赖不能直接创建,例如数据库连接、文件I/O等。这种场景就非常适合使用 mock/stub 测试。简单来说,就是用
mock 对象模拟依赖项的行为。
gomock地址:https://github.com/golang/mock
针对 interface 生成对应的 Mock 代码文件,其中包含了一个实现该接口的结构,并提供了操作该结构行为的方
法。使用该结构代替真实的依赖,可以控制下游按我们想要的方式进行某些操作和返回结果,以此达到解除外部依
赖的目的。
$ go version
go version go1.18.4 windows/amd64
# GoMock安装
$ go get -u github.com/golang/mock/gomock
# mockgen辅助代码生成工具安装
$ go get -u github.com/golang/mock/mockgen
$ go install github.com/golang/mock/mockgen
$ mockgen -version
v1.6.0
GoMock文档:
$ go doc github.com/golang/mock/gomock
mockgen 工具支持的选项如下:
-source
:指定接口的源文件。
-destinatio
:mock类代码的输出文件,如果没有设置本选项,代码将被输出到标准输出。-destination选项输
入太长,因此推荐使用重定向符号>将输出到标准输出的内容重定向到某个文件,并且mock类代码的输出文件的
路径必须是绝对路径。
-packag
:指定 mock 类源文件的包名,如果没有设置本选项,则包名由 mock_ 和输入文件的包名级联而成。
-aux_fi
:附加文件列表用于解析嵌套定义在不同文件中的 interface,指定元素列表以逗号分隔,元素形式为
foo=bar/baz.go,其中bar/baz.go是源文件,foo是-source选项指定的源文件用到的包名。
-build_flags
:传递给 build 工具的参数。
-imports
:依赖的需要 import 的包,在生成的源代码中应该使用的一个显式导入列表,指定为一个以逗号分隔
的元素列表,形式为foo=bar/baz,其中bar/baz是被导入的包,foo是生成的源代码中包使用的标识符。
-mock_names
:自定义生成 mock 文件的列表,使用逗号分割。
如 Repository=MockSensorRepository,Endpoint=MockSensorEndpoint。Repository、Endpoint为接口,
MockSensorRepository,MockSensorEndpoint 为相应的 mock 文件。
-self_package
:生成代码的完整包导入路径,这个标志的目的是通过尝试包含它自己的包来防止生成代码中的
导入死循环。如果 mock 的包被设置为它的一个输入(通常是主输入),并且输出是 stdio,因此 mockgen 无法检测
到最终的输出包,就会发生这种情况,设置这个标志将告诉 mockgen 要排除哪个导入。
-copyright_file
:用于向生成的源代码中添加版权头的版权文件。
-debug_parser
:只打印解析器结果。
-exec_only
:(反射模式)如果设置,执行反射程序。
-prog_only
:(反射模式)只生成反射程序,将其写入stdout并退出。
-write_package_comment
:如果为true,编写包文档注释(godoc),默认为true。
mockgen 有两种操作模式:源文件模式和反射模式。
源文件模式通过一个包含 interface 定义的源文件生成 mock 类文件,通过 -source 标识开启,-imports 和
-aux_files 标识在源文件模式下是有用的。mockgen源文件模式的命令格式如下:
mockgen -source=xxxx.go [other options]
反射模式通过构建一个程序用反射理解接口生成一个 mock 类文件,通过两个非标志参数开启:导入路径和用逗
号分隔的符号列表(多个interface)。反射模式的命令格式如下:
mockgen packagepath Interface1,Interface2...
第一个参数是基于 GOPATH 的相对路径,第二个参数可以为多个 interface 并且 interface 之间只能用逗号分隔。
mockgen database/sql/driver Conn,Driver
# 可以使用 . 表示当前路径的包
mockgen . Conn,Driver
mockgen 工作模式适用场景如下:
A、对于简单场景,只需使用 -source 选项。
B、对于复杂场景,如一个源文件定义了多个 interface 而只想对部分 interface 进行 mock,或者 interface 存在
嵌套,则需要使用反射模式。
Call 表示对 mock 对象的一个期望调用,Call 结构体为:
type Call struct {
t TestReporter // for triggering test failures on invalid call setup
receiver interface{} // the receiver of the method call
method string // the name of the method
methodType reflect.Type // the type of the method
args []Matcher // the args
origin string // file and line number of call setup
preReqs []*Call // prerequisite calls
// Expectations
minCalls, maxCalls int
numCalls int // actual number made
// actions are called when this Call is called. Each action gets the args and
// can set the return values by returning a non-nil slice. Actions run in the
// order they are created.
actions []func([]interface{}) []interface{}
}
常用方法:
// InOrder声明给定调用的调用顺序
func InOrder(calls ...*Call)
// After声明调用在preReq完成后执行
func (c *Call) After(preReq *Call) *Call
// 允许调用0次或多次
func (c *Call) AnyTimes() *Call
// 声明在匹配时要运行的操作
func (c *Call) Do(f interface{}) *Call
// 设置最大的调用次数为n次
func (c *Call) MaxTimes(n int) *Call
// 设置最小的调用次数为n次
func (c *Call) MinTimes(n int) *Call
// Return声明模拟函数调用返回的值
func (c *Call) Return(rets ...interface{}) *Call
// SetArg声明使用指针设置第n个参数的值
func (c *Call) SetArg(n int, value interface{}) *Call
// 设置调用的次数为n次
func (c *Call) Times(n int) *Call
// 获取控制对象
func NewController(t TestReporter) *Controller
// WithContext返回一个控制器和上下文,如果发生任何致命错误时会取消
func WithContext(ctx context.Context, t TestReporter) (*Controller, context.Context)
// Mock对象调用,不应由用户代码调用
func (ctrl *Controller) Call(receiver interface{}, method string, args ...interface{}) []interface{}
// 检查所有预计调用的方法是否被调用,每个控制器都应该调用,本函数只应该被调用一次
func (ctrl *Controller) Finish()
// 被mock对象调用,不应由用户代码调用
func (ctrl *Controller) RecordCall(receiver interface{}, method string, args ...interface{}) *Call
// 被mock对象调用,不应由用户代码调用
func (ctrl *Controller) RecordCallWithMethodType(receiver interface{}, method string, methodType reflect.Type, args ...interface{}) *Call
// 匹配任意值
func Any() Matcher
// AssignableToTypeOf是一个匹配器,用于匹配赋值给模拟调用函数的参数和函数的参数类型是否匹配
func AssignableToTypeOf(x interface{}) Matcher
// 通过反射匹配到指定的类型值,而不需要手动设置
func Eq(x interface{}) Matcher
// 返回nil
func Nil() Matcher
// 不递归给定子匹配器的结果
func Not(x interface{}) Matcher
使用 gomock 的时候有几个步骤:
让我们通过一个简单的例子来演示 gomock 的整个使用流程,为简单起见我们只看两个文件,一个是接口文件,
user/inter.go
,是我们希望 mock 的接口;另一个是结构体文件,user/user.go
,是使用了 iuser 接口的结
构体 User。
package user
type Inter interface {
DoResData(int, string) error
}
package user
type User struct {
Inter Inter
}
func (user *User) Use() error {
return user.Inter.DoResData(123, "Hello GoMock")
}
先在项目的根目录下创建一个 mocks 包,然后通过 mockgen 生成对 Inter 接口的 mock 方法,在 user 的测试中
使用。按照下面的步骤来:
$ mockgen -destination="mocks/mock_inter.go" -package="mocks" -source ./user/inter.go Inter
生成 mock 文件的各参数如下:
-source
:指定需要模拟(mock)的接口文件。-destination
:设置生成的mock文件名,若不设置则打印到标准输出中。-packge
:设置 mock 文件的包名,若不设置,则为 mock_
前缀加文件名。我们不用关心生成的 mock 文件细节,只需要知道怎么使用即可。
生成的 mocks/mock_inter.go
文件的内容:
// Code generated by MockGen. DO NOT EDIT.
// Source: ./user/inter.go
// Package mocks is a generated GoMock package.
package mocks
import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
)
// MockInter is a mock of Inter interface.
type MockInter struct {
ctrl *gomock.Controller
recorder *MockInterMockRecorder
}
// MockInterMockRecorder is the mock recorder for MockInter.
type MockInterMockRecorder struct {
mock *MockInter
}
// NewMockInter creates a new mock instance.
func NewMockInter(ctrl *gomock.Controller) *MockInter {
mock := &MockInter{ctrl: ctrl}
mock.recorder = &MockInterMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockInter) EXPECT() *MockInterMockRecorder {
return m.recorder
}
// DoResData mocks base method.
func (m *MockInter) DoResData(arg0 int, arg1 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DoResData", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// DoResData indicates an expected call of DoResData.
func (mr *MockInterMockRecorder) DoResData(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DoResData", reflect.TypeOf((*MockInter)(nil).DoResData), arg0, arg1)
}
package user_test
import (
"github.com/golang/mock/gomock"
"proj/mocks"
"proj/user"
"testing"
)
func TestUse(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
// 先设置期望的返回结果
mockInter := mocks.NewMockInter(mockCtrl)
mockInter.EXPECT().DoResData(123, "Hello GoMock").Return(nil).Times(1)
// 再调用方法
testUser := &user.User{Inter: mockInter}
err := testUser.Use()
if err != nil {
t.Errorf("result = %v", err)
}
}
进行测试:
$ go test -v
=== RUN TestUse
--- PASS: TestUse (0.00s)
PASS
ok proj/user 0.026s
对上面代码的说明:
1 、gomock.NewController(t)
:Controller 表示模拟生态系统的顶级控制,它定义模拟对象的作用域、生命周
期,以及它们的期望。从多个 goroutine 调用 Controller 的方法是线程安全的,每个测试都应该创建一个新的
Controller,并且通过 defer 调用它的 Finish 方法。
2、mocks.NewMockInter(mockCtrl)
:创建一个模拟(mock)的 Inter 对象。
3、EXPECT()
:返回一个 erro 对象,该对象允许调用者设置期望的返回值。
4、DoResData()
:设置入参并调用 mock 对象中的方法。
5、gomock.Any()
:可以用来匹配任意入参,上面使用的是固定参数。
6、Return()
:设置返回值。
7、Times(1)
:调用一次,也可以设置成 AnyTimes()
进行无限次调用,也就是不管调用多少次都会返回这个结
果。
8、注意在 go 1.14+ 时,如果已经将 *testing.T
对象传入 Controller,可以不用主动调用 Finish()
。
再看一个简单的例子:
package db
type DB interface {
Get(key string) (int, error)
}
func GetFromDB(db DB, key string) int {
if value, err := db.Get(key); err == nil {
return value
}
return -1
}
生成 mock :
$ mockgen -destination="mocks/mock_db.go" -package="mocks" -source ./db/db.go DB
生成的文件 mocks/db.go
的内容为:
// Code generated by MockGen. DO NOT EDIT.
// Source: ./db/db.go
// Package mocks is a generated GoMock package.
package mocks
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)
}
编写测试类:
package db_test
import (
"errors"
"github.com/golang/mock/gomock"
"proj/db"
"proj/mocks"
"testing"
)
func TestGetFromDB(t *testing.T) {
ctrl := gomock.NewController(t)
// 断言DB.Get()方法是否被调用
defer ctrl.Finish()
// 如果DB.Get()返回error,那么GetFromDB()返回-1
m := mocks.NewMockDB(ctrl)
m.EXPECT().Get(gomock.Eq("Tom")).Return(100, errors.New("not exist"))
if v := db.GetFromDB(m, "Tom"); v != -1 {
t.Fatal("expected -1, but got", v)
}
}
在上面的例子中,当 Get()
的参数为 Tom,则返回 error,这称之为 打桩(stub)
,有明确的参数和返回值是最
简单打桩方式。除此之外,检测调用次数、调用顺序,动态设置返回值等方式也经常使用。
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 值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")
})
Retur
:返回确定的值。Do
:Mock 方法被调用时,要执行的操作吗,忽略返回值。DoAndRetur
:可以动态地控制返回值。func TestGetFromDB(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
m := mocks.NewMockDB(ctrl)
m.EXPECT().Get(gomock.Not("Sam")).Return(0, nil).Times(2)
db.GetFromDB(m, "ABC")
db.GetFromDB(m, "DEF")
}
Times()
:断言 Mock 方法被调用的次数。MaxTimes()
:最大次数。MinTimes()
:最小次数。AnyTimes()
:任意次数(包括 0 次)。默认情况下,预期的调用不会强制以任何特定的顺序运行。
Call
顺序依赖关系可以通过使用 InOrder
和或 Call.After()
来强制约束。
Call.After()
调用可以创建更多样化的调用顺序依赖关系,但 InOrder
通常更方便。
func TestGetFromDB(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish() // 断言 DB.Get() 方法是否被调用
m := mocks.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)
db.GetFromDB(m, "Tom")
db.GetFromDB(m, "Sam")
}
firstCall := mockObj.EXPECT().SomeMethod(1, "first")
secondCall := mockObj.EXPECT().SomeMethod(2, "second").After(firstCall)
mockObj.EXPECT().SomeMethod(3, "third").After(secondCall)
gomock.InOrder(
mockObj.EXPECT().SomeMethod(1, "first"),
mockObj.EXPECT().SomeMethod(2, "second"),
mockObj.EXPECT().SomeMethod(3, "third"),
)
生成测试覆盖率的 profile 文件:
go test -coverprofile=cover.out .
利用 profile 文件生成可视化界面:
go tool cover -html=cover.out