1. go mock单元测试
搞单元测试,如果碰到这些情况:
- 一个函数,内部包含了很多并且很深的调用,但是如果单单测这个函数,其实实现的功能很简单。
- 一个函数,包含了其他还未实现的调用。
- 函数内部对数据的要求极为苛刻。
那么这时候就可以考虑使用mock来处理。
mock,简而言之就是可以通过注入我们所期望返回的数据,或者我们所期望传递的参数,来避免上面那些情况,其原理则是通过反射来实现。
2. go mock下载
gomock是go官方提供的mock解决方案,主要分为两部分:gomock库和mock代码生成工具mockgen。首先你需要做的是将依赖下载到本地:
1 2 |
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代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
//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的文件。123mockgen -source=foo.go [other options]#这里执行如下:mockgen -package="mock1" -source="./metal.go" > mock_metal.go -
reflect模式
一个文件定义了多个interface而你只想对部分interface进行mock,或者interface存在嵌套,使用reflect模式1mockgen 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
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的函数会按照我们定义的行为来执行。结果如下:
1 2 3 4 5 6 7 |
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. 接口文件
如果有接口文件,则可以通过:
1 2 3 4 5 6 |
-source: 指定接口文件 -destination: 生成的文件名 -package:生成文件的包名 -imports: 依赖的需要import的包 -aux_files:接口文件不止一个文件时附加文件 -build_flags: 传递给build工具的参数 |
比如mock代码使用
1 |
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 "工具。
在接口文件的注释里面增加如下:
1 |
//go:generate mockgen -destination mock_spider.go -package spider github.com/cz-it/blog/blog/Go/testing/gomock/example/spider Spider |
这样,只要在spider目录下执行
1 |
go generate |
命令就可以自动生成mock文件了。
5. gomock接口使用
在生成了mock实现代码之后,我们就可以进行正常使用了。这里假设结合testing进行使用(当然你也可考虑使用GoConvey)。我们就可以
在单元测试代码里面首先创建一个mock控制器:
1 |
mockCtl := gomock.NewController(t) |
将* testing.T传递给gomock生成一个"Controller"对象,该对象控制了整个Mock的过程。在操作完后还需要进行回收,所以一般会在New后面defer一个Finish
1 |
defer mockCtl.Finish() |
然后就是调用mock生成代码里面为我们实现的接口对象:
1 |
mockMetal := spider.NewmockMetal(mockCtl) |
这里的"spider"是mockgen命令里面传递的报名,后面是NewMockXxxx格式的对象创建函数"Xxx"是接口名。这里需要传递控制器对象进去。返回一个接口的实现对象。
有了实现对象,我们就可以调用其断言方法了:EXPECT()
这里gomock非常牛的采用了链式调用法,和Swfit以及ObjectiveC里面的Masonry库一样,通过"."连接函数调用,可以像链条一样连接下去。
1 |
mockMetal.EXPECT().GetBody().Return("go1.8.3") |
这里的每个"."调用都得到一个"Call"对象,该对象有如下方法:
1 2 3 4 5 6 7 8 9 |
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函数,这里还可以有另外几种限制:
1 2 3 |
AnyTimes() : 0到多次 MaxTimes(n int) :最多执行n次,如果没有设置 MinTimes(n int) :最少执行n次,如果没有设置 |
5.3. 指定执行顺序
有时候我们还要指定执行顺序,比如要先执行Init操作,然后才能执行Recv操作。
1 2 |
initCall := mockMetal.EXPECT().Init() mockMetal.EXPECT().Recv().After(initCall) |
6. 参考文章
- 官方gomock库的所有方法说明 :gomock库的所有方法说明
- 官方例子:官方的例子,里面有如何进行gomock的方法使用
- golang单元测试之mock
- 使用Golang的官方mock工具--gomock
微信赞赏
支付宝赞赏