gomock是官方提供的mock框架,用于解决单元测试中遇到的外部依赖问题,并且还有mockgen工具用来辅助生成相关的mock代码。
gomock的原理是:针对 interface 生成对应的Mock代码文件,其中包含了一个实现该接口的结构,并提供了操作该结构行为的方法。使用该结构代替真实的依赖,可以控制下游按我们想要的方式进行某些操作和返回结果,以此达到解除外部依赖的目的。
1. 安装gomock和mockgen
# 安装gomock和mockgen
go get -u github.com/golang/mock/gomock
go install github.com/golang/mock/mockgen
安装成功后可以在命令行运行mockgen命令查看是否安装成功。
2. 具体使用方式
# 演示用例的目录结构
.
├── dao
│ └── dao.go
├── go.mod
├── go.sum
├── mock_dao
│ └── mock_dao.go
└── service
├── service.go
└── service_test.go
演示用例的目录结构如上,去除UT相关的内容,该项目最基本的结构包含dao和service。其中dao代表的是数据层,其中定义了一个Search接口,具体实现应该是要去mysql中查找数据;service代表的是业务逻辑层,会调用dao层的接口进一步实现具体业务。
这是一个比较典型的依赖数据库的例子,可以使用gomock对dao中的接口进行mock。
//dao和service中具体的代码
// dao.dao.go
type Search interface{
GetNameByID(id int64) (string, error)
}
// service.service.go
type FindService struct {
DB dao.Search
}
func (f *FindService) FindUser(id int64) string {
name, _ := f.DB.GetNameByID(id)
return name
}
- 首先,运行以下命令,在mock_dao目录下生成mock_dao.go文件。
mockgen -source=dao.go -destination=../mock_dao/mock_dao.go -package=mock_dao
- 其次,编写service_test.go文件,具体如下。
package service
import (
"github.com/golang/mock/gomock"
"gomock/mock_dao"
"testing"
)
func TestFindUser(t *testing.T) {
// 首先生成一个controller对象,然后注册到defer中
ctl := gomock.NewController(t)
defer ctl.Finish()
// 然后生成一个mockSearch对象,用来替代Search接口
mockSearch := mock_dao.NewMockSearch(ctl)
service := FindService{DB: mockSearch}
// 操纵mockSearch对象的行为
mockSearch.EXPECT().GetNameByID(int64(10)).Return("liangPin", nil)
mockSearch.EXPECT().GetNameByID(int64(20)).Return("zhangHeng", nil)
// 测试过程
if name1 := service.FindUser(10); name1 != "liangPin"{
t.Error(name1)
}
if name2 := service.FindUser(20); name2 != "zhangHeng"{
t.Error(name2)
}
}
3. gomock源代码解析
- Controller对象—mock对象的核心
Controller控制着mock对象的作用域和生命周期,我们操纵mock对象的行为也是将其加入到expectedCalls中,所以Controller是非常核心的一个对象。每个测试都应该创建一个controller对象将其注册到defer中。通常通过NewController函数来创建controller,1.14版本以后通过NewController创建的controller可以不必显式地调用ctrl.Finish()。
// Controller对象的定义
type Controller struct {
// 传入的test对象
T TestHelper
// 保证多协程安全的
mu sync.Mutex
// mock对象的期望行为
expectedCalls *callSet
finished bool
}
// 使用NewController来得到Controller
func NewController(t TestReporter) *Controller {
h, ok := t.(TestHelper)
if !ok {
h = &nopTestHelper{t}
}
ctrl := &Controller{
T: h,
expectedCalls: newCallSet(),
}
if c, ok := isCleanuper(ctrl.T); ok {
c.Cleanup(func() {
ctrl.T.Helper()
ctrl.finish(true, nil)
})
}
return ctrl
}
- Call对象—预期的mock对象行为
Call对象是我们预期的mock对象的行为。UT代码中我们预期的行为会创建一个Call对象传入到Controller中,测试过程调用时会去Controller的excepedCalls中匹配找到对应的Call对象,并返回结果。
// Call对象的定义
type Call struct {
t TestHelper // 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{}
}