1. go mock单元测试
搞单元测试,如果碰到这些情况:
- 一个函数,内部包含了很多并且很深的调用,但是如果单单测这个函数,其实实现的功能很简单。
- 一个函数,包含了其他还未实现的调用。
- 函数内部对数据的要求极为苛刻。
那么这时候就可以考虑使用mock来处理。
mock,简而言之就是可以通过注入我们所期望返回的数据,或者我们所期望传递的参数,来避免上面那些情况,其原理则是通过反射来实现。
2. go mock下载
gomock是go官方提供的mock解决方案,主要分为两部分:gomock库和mock代码生成工具mockgen。首先你需要做的是将依赖下载到本地:
go get github.com/golang/mock/gomock #代码依赖
go get github.com/golang/mock/mockgen #命令行工具
如何你设置过$GOPATH/bin到你的$PATH变量中,那么这里就可以直接运行mockgen命令了,否则需要使用绝对路径或者相当于$GOPATH的目录。
3. mock示例
└── mock1
├── metal_test.go
├── metal.go
└── mock_metal.go //通过mockgen自动生成
3.1. 示例代码metal.go
metal.go代码
//go: generate mockgen -package="mock1" -source="./metal.go" > mock_metal.go
package mock1
type Imetal interface {
GetName() string
SetName(string) string
}
func GetMetalName(mi Imetal) string {
mi.GetName()
return mi.GetName()
}
func SetMetalName(mi Imetal,name string) string {
return mi.SetName(name)
}
type Metal struct {
Name string
Exchange string
}
func (self Metal) GetName() string {
if self.Name==""{
return "none"
}
return self.Name
}
func (self *Metal) SetName(brand string) string {
self.Name=brand
return "done"
}
我现在有一个package,其包含了IMetal接口,这个接口下面有两个方法,现在针对这两个方法来进行mock,ps:gomock只支持interface方法的mock。
在mock之前,需要先通过mockgen来生成mock代码,我的源就是上面的IMeta接口。
3.2. 生成mock代码mock_metal.go
首先用gomock提供的mockgen工具生成要mock的接口的实现,这个工具是gomock提供的用来为要mock的接口生成实现的。它可以根据给定的接口,来自动生成代码。mockgen有两种工作模式:source和reflect
-
source模式
根据源文件来生成,源文件是包含了一个或多个interface的文件。mockgen -source=foo.go [other options] #这里执行如下: mockgen -package="mock1" -source="./metal.go" > mock_metal.go
-
reflect模式
一个文件定义了多个interface而你只想对部分interface进行mock,或者interface存在嵌套,使用reflect模式mockgen src/package Conn,Driver
如果编译报错read F:\Golang\src\test\mock1\mock_metal.go: unexpected NUL in input
, 是因为windows下生成UTF-16字符文件,需要修改生成的字符为UTF-8格式,可以通过nodepad++进行修改转换格式;
mock代码生成好之后,接下来是写测试函数。
3.3. 测试函数metal_test.go
package mock1
import (
"fmt"
"testing"
"github.com/golang/mock/gomock"
)
func TestMetalName(t *testing.T) {
mockCtl := gomock.NewController(t)
defer mockCtl.Finish()
mockMetal := NewMockImetal(mockCtl) //NewMockImetal是生成的mock代码,以包的形式存在
m := new(Metal)
//获取metal name测试
mockCtl.RecordCall(m, "GetName")
mockCtl.Call(m, "GetName")
mockMetal.EXPECT().GetName().Return("apple").MaxTimes(10) //call :=
//mockMetal.EXPECT().GetName().Return("peer").After(call) //注入期望的返回值
mockedBrand := GetMetalName(mockMetal)
//设置MetalName测试
mockMetal.EXPECT().SetName(gomock.Eq("al")).Do(func(format string) { //入参校验
fmt.Println("recv param :", format)
}).Return("setdone")
mockedSetName := SetMetalName(mockMetal, "al")
fmt.Println(mockedSetName)
if "peer" != mockedBrand {
t.Error("Get wrong name:", mockedBrand)
}
if "setdone" != mockedSetName {
t.Error("Set wrong name:", mockedSetName)
}
}
然后执行go test即可,会发现这些被mock的函数会按照我们定义的行为来执行。结果如下:
recv param : al
setdone
--- FAIL: TestMetalName (0.00s)
metal_test.go:39: Get wrong name: apple
FAIL
exit status 1
FAIL test/mock1 0.297s
4. mockgen工具使用
在生成mock代码的时候,我们用到了mockgen工具,这个工具是gomock提供的用来为要mock的接口生成实现的。它可以根据给定的接口,来自动生成代码。这里给定接口有两种方式:接口文件和实现文件
4.1. 接口文件
如果有接口文件,则可以通过:
-source: 指定接口文件
-destination: 生成的文件名
-package:生成文件的包名
-imports: 依赖的需要import的包
-aux_files:接口文件不止一个文件时附加文件
-build_flags: 传递给build工具的参数
比如mock代码使用
mockgen -destination spider/mock_spider.go -package spider -source spider/spider.go
就是将接口spider/spider.go中的接口做实现并存在 spider/mock_spider.go文件中,文件的包名为"spider"。
4.2. 实现文件
在我们的上面的例子中,并没有使用"-source",那是如何实现接口的呢?mockgen还支持通过反射的方式来找到对应的接口。只要在所有选项的最后增加一个包名和里面对应的类型就可以了。其他参数和上面的公用。
4.3. 通过注释指定mockgen
如上所述,如果有多个文件,并且分散在不同的位置,那么我们要生成mock文件的时候,需要对每个文件执行多次mockgen命令(假设包名不相同)。这样在真正操作起来的时候非常繁琐,mockgen还提供了一种通过注释生成mock文件的方式,此时需要借助go的"go generate "工具。
在接口文件的注释里面增加如下:
//go:generate mockgen -destination mock_spider.go -package spider github.com/cz-it/blog/blog/Go/testing/gomock/example/spider Spider
这样,只要在spider目录下执行
go generate
命令就可以自动生成mock文件了。
5. gomock接口使用
在生成了mock实现代码之后,我们就可以进行正常使用了。这里假设结合testing进行使用(当然你也可考虑使用GoConvey)。我们就可以
在单元测试代码里面首先创建一个mock控制器:
mockCtl := gomock.NewController(t)
将* testing.T传递给gomock生成一个"Controller"对象,该对象控制了整个Mock的过程。在操作完后还需要进行回收,所以一般会在New后面defer一个Finish
defer mockCtl.Finish()
然后就是调用mock生成代码里面为我们实现的接口对象:
mockMetal := spider.NewmockMetal(mockCtl)
这里的"spider"是mockgen命令里面传递的报名,后面是NewMockXxxx格式的对象创建函数"Xxx"是接口名。这里需要传递控制器对象进去。返回一个接口的实现对象。
有了实现对象,我们就可以调用其断言方法了:EXPECT()
这里gomock非常牛的采用了链式调用法,和Swfit以及ObjectiveC里面的Masonry库一样,通过"."连接函数调用,可以像链条一样连接下去。
mockMetal.EXPECT().GetBody().Return("go1.8.3")
这里的每个"."调用都得到一个"Call"对象,该对象有如下方法:
func (c *Call) After(preReq *Call) *Call
func (c *Call) AnyTimes() *Call
func (c *Call) Do(f interface{}) *Call
func (c *Call) MaxTimes(n int) *Call
func (c *Call) MinTimes(n int) *Call
func (c *Call) Return(rets ...interface{}) *Call
func (c *Call) SetArg(n int, value interface{}) *Call
func (c *Call) String() string
func (c *Call) Times(n int) *Call
这里EXPECT()得到实现的对象,然后调用实现对象的接口方法,接口方法返回第一个"Call"对象,
然后对其进行条件约束。
上面约束都可以在文档中或者根据字面意思进行理解,这里列举几个例子:
5.1. 指定返回值
如我们的例子,调用Call的Return函数,可以指定接口的返回值:
mockMetal.EXPECT().GetBody().Return("go1.8.3")
这里我们指定返回接口函数GetBody()返回"go1.8.3"。
5.2. 指定执行次数
有时候我们需要指定函数执行多次,比如接受网络请求的函数,计算其执行了多少次。
mockMetal.EXPECT().Recv().Return(nil).Times(3)
执行三次Recv函数,这里还可以有另外几种限制:
AnyTimes() : 0到多次
MaxTimes(n int) :最多执行n次,如果没有设置
MinTimes(n int) :最少执行n次,如果没有设置
5.3. 指定执行顺序
有时候我们还要指定执行顺序,比如要先执行Init操作,然后才能执行Recv操作。
initCall := mockMetal.EXPECT().Init()
mockMetal.EXPECT().Recv().After(initCall)
6. 参考文章
- 官方gomock库的所有方法说明 :gomock库的所有方法说明
- 官方例子:官方的例子,里面有如何进行gomock的方法使用
- golang单元测试之mock
- 使用Golang的官方mock工具--gomock