全局变量可通过GoStub框架打桩
过程可通过GoStub框架打桩
函数可通过GoStub框架打桩
interface可通过GoMock框架打桩
mockgen has two modes of operation: source and reflect. Source mode generates mock interfaces from a source file.
Reflect mode generates mock interfaces by building a program that uses reflection to understand interfaces.
gomock主要包含两个部分:” gomock库”和“ 辅助代码生成工具mockgen”
他们都可以通过go get来获取:
go get github.com/golang/mock/gomock
go get github.com/golang/mock/mockgen
文档
GoMock框架安装完成后,可以使用go doc命令来获取文档:
go doc github.com/golang/mock/gomock
另外,有一个在线的参考文档,即package gomock。
使用方法
定义一个接口
我们先定义一个打算mock的接口Repository:
package db
type Repository interface {
Create(key string, value []byte) error
Retrieve(key string) ([]byte, error)
Update(key string, value []byte) error
Delete(key string) error
}
Repository是领域驱动设计中战术设计的一个元素,用来存储领域对象,一般将对象持久化在数据库中,比如Aerospike,Redis或Etcd等。对于领域层来说,只知道对象在Repository中维护,并不care对象到底在哪持久化,这是基础设施层的职责。微服务在启动时,根据部署参数实例化Repository接口,比如AerospikeRepository,RedisRepository或EtcdRepository。
假设有一个领域对象Movie要进行持久化,则先要通过json.Marshal进行序列化,然后再调用Repository的Create方法来存储。当要根据key(实体Id)查找领域对象时,则先通过Repository的Retrieve方法获得领域对象的字节切片,然后通过json.Unmarshal进行反序列化的到领域对象。当领域对象的数据有变化时,则先要通过json.Marshal进行序列化,然后再调用Repository的Update方法来更新。当领域对象生命周期结束而要消亡时,则直接调用Repository的Delete方法进行删除。
生成mock类文件
这下该mockgen工具登场了。mockgen有两种操作模式:源文件和反射。
源文件模式通过一个包含interface定义的文件生成mock类文件,它通过 -source 标识生效,-imports 和 -aux_files 标识在这种模式下也是有用的。
举例:
mockgen -source=foo.go [other options]
反射模式通过构建一个程序用反射理解接口生成一个mock类文件,它通过两个非标志参数生效:导入路径和用逗号分隔的符号列表(多个interface)。
举例:
mockgen database/sql/driver Conn,Driver
注意:第一个参数是基于GOPATH的相对路径,第二个参数可以为多个interface,并且interface之间只能用逗号分隔,不能有空格。
有一个包含打算Mock的interface的源文件,就可用mockgen命令生成一个mock类的源文件。mockgen支持的选项如下:
-source: 一个文件包含打算mock的接口列表
-destination: 存放mock类代码的文件。如果你没有设置这个选项,代码将被打印到标准输出
-package: 用于指定mock类源文件的包名。如果你没有设置这个选项,则包名由mock_和输入文件的包名级联而成
-aux_files: 参看附加的文件列表是为了解析类似嵌套的定义在不同文件中的interface。指定元素列表以逗号分隔,元素形式为foo=bar/baz.go,其中bar/baz.go是源文件,foo是-source选项指定的源文件用到的包名
在简单的场景下,你将只需使用-source选项。在复杂的情况下,比如一个文件定义了多个interface而你只想对部分interface进行mock,或者interface存在嵌套,这时你需要用反射模式。
完整命令
$ mockgen -source db.go -package db -destination db_test.go
//注意source模式下,destination 文件必需为空,否则报错:
$ mockgen -package db -destination db_interface_test.go database/sql/driver Conn,Driver
//interface模式下,destination 文件必需在前
通过注释指定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文件了。
package db
//go:generate mockgen -destination mock_genenrate_test.go -package db database/sql/driver Conn,Driver
type Repository interface {
}
$ go generate
gomock的接口使用
在生成了mock实现代码之后,我们就可以进行正常使用了。这里假设结合testing进行使用(当然你也可考虑使用GoConvey)。我们就可以
在单元测试代码里面首先创建一个mock控制器:
mockCtl := gomock.NewController(t)
将* testing.T传递给gomock生成一个”Controller”对象,该对象控制了整个Mock的过程。在操作完后还需要进行回收,所以一般会在New后面defer一个Finish
defer mockCtl.Finish()
然后就是调用mock生成代码里面为我们实现的接口对象:
mockSpider := spider.NewMockSpider(mockCtl)
这里的”spider”是mockgen命令里面传递的报名,后面是NewMockXxxx格式的对象创建函数”Xxx”是接口名。这里需要传递控制器对象进去。返回一个接口的实现对象。
有了实现对象,我们就可以调用其断言方法了:EXPECT()
这里gomock非常牛的采用了链式调用法,和Swfit以及ObjectiveC里面的Masonry库一样,通过”.”连接函数调用,可以像链条一样连接下去。
mockSpider.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”对象,
然后对其进行条件约束。
上面约束都可以在文档中或者根据字面意思进行理解,这里列举几个例子:
指定返回值
如我们的例子,调用Call的Return函数,可以指定接口的返回值:
mockSpider.EXPECT().GetBody().Return(“go1.8.3”)
这里我们指定返回接口函数GetBody()返回”go1.8.3”。
指定执行次数
有时候我们需要指定函数执行多次,比如接受网络请求的函数,计算其执行了多少次。
mockSpider.EXPECT().Recv().Return(nil).Times(3)
执行三次Recv函数,这里还可以有另外几种限制:
AnyTimes() : 0到多次
MaxTimes(n int) :最多执行n次,如果没有设置
MinTimes(n int) :最少执行n次,如果没有设置
指定执行顺序
有时候我们还要指定执行顺序,比如要先执行Init操作,然后才能执行Recv操作。
initCall := mockSpider.EXPECT().Init()
mockSpider.EXPECT().Recv().After(initCall)
//go build github.com/xiazemin/mock/gomock/db/source: no non-test Go files in /Users/didi/goLang/src/github.com/xiazemin/mock/gomock/db/source
生成的mock文件需要被引用,所以不能是_test.go 结尾
使用mock对象进行打桩测试
mock类源文件生成后,就可以写测试用例了。
导入mock相关的包
mock相关的包包括testing,gmock和mock_db,import包路径:
import (
“testing”
. “github.com/golang/mock/gomock”
“test/mock”
…
)
mock控制器
mock控制器通过NewController接口生成,是mock生态系统的顶层控制,它定义了mock对象的作用域和生命周期,以及它们的期望。多个协程同时调用控制器的方法是安全的。
当用例结束后,控制器会检查所有剩余期望的调用是否满足条件。
控制器的代码如下所示:
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mock对象创建时需要注入控制器,如果有多个mock对象则注入同一个控制器,如下所示:
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockRepo := mock_db.NewMockRepository(ctrl)
mockHttp := mock_api.NewHttpMethod(ctrl)
mock对象的行为注入
对于mock对象的行为注入,控制器是通过map来维护的,一个方法对应map的一项。因为一个方法在一个用例中可能调用多次,所以map的值类型是数组切片。当mock对象进行行为注入时,控制器会将行为Add。当该方法被调用时,控制器会将该行为Remove。
假设有这样一个场景:先Retrieve领域对象失败,然后Create领域对象成功,再次Retrieve领域对象就能成功。这个场景对应的mock对象的行为注入代码如下所示:
mockRepo.EXPECT().Retrieve(Any()).Return(nil, ErrAny)
mockRepo.EXPECT().Create(Any(), Any()).Return(nil)
mockRepo.EXPECT().Retrieve(Any()).Return(objBytes, nil)
objBytes是领域对象的序列化结果,比如:
obj := Movie{…}
objBytes, err := json.Marshal(obj)
…
当批量Create对象时,可以使用Times关键字:
mockRepo.EXPECT().Create(Any(), Any()).Return(nil).Times(5)
当批量Retrieve对象时,需要注入多次mock行为:
mockRepo.EXPECT().Retrieve(Any()).Return(objBytes1, nil)
mockRepo.EXPECT().Retrieve(Any()).Return(objBytes2, nil)
mockRepo.EXPECT().Retrieve(Any()).Return(objBytes3, nil)
mockRepo.EXPECT().Retrieve(Any()).Return(objBytes4, nil)
mockRepo.EXPECT().Retrieve(Any()).Return(objBytes5, nil)
行为调用的保序
默认情况下,行为调用顺序可以和mock对象行为注入顺序不一致,即不保序。如果要保序,有两种方法:
通过After关键字来实现保序
通过InOrder关键字来实现保序
通过After关键字实现的保序示例代码:
firstCall := mockObj.EXPECT().SomeMethod(1, “first”)
secondCall := mockObj.EXPECT().SomeMethod(2, “second”).After(firstCall)
mockObj.EXPECT().SomeMethod(3, “third”).After(secondCall)
通过InOrder关键字实现的保序示例代码:
InOrder(
mockObj.EXPECT().SomeMethod(1, “first”),
mockObj.EXPECT().SomeMethod(2, “second”),
mockObj.EXPECT().SomeMethod(3, “third”),
)
显然,InOrder关键字实现的保序更简单自然,所以推荐这种方式。其实,关键字InOrder是After的语法糖,不信你看:
// InOrder declares that the given calls should occur in order.
func InOrder(calls …*Call) {
for i := 1; i < len(calls); i++ {
calls[i].After(calls[i-1])
}
}
当mock对象行为的注入保序后,如果行为调用的顺序和其不一致,则测试失败。这就是说,对于上面的例子,如果在测试用例执行过程中,SomeMethod方法的调用不是按照SomeMethod(1, “first”) -> SomeMethod(2, “second”) -> SomeMethod(3, “third”) 的顺序进行,则测试失败。
mock对象的注入
mock对象的行为都注入到控制器以后,我们接着要将mock对象注入给interface,使得mock对象在测试中生效。
在使用GoStub框架之前,很多人都使用土方法,比如Set。这种方法有一个缺陷:当测试用例执行完成后,并没有回滚interface到真实对象,有可能会影响其它测试用例的执行。所以,笔者强烈建议大家使用GoStub框架完成mock对象的注入。
stubs := StubFunc(&redisrepo.GetInstance, mockDb)
defer stubs.Reset()
测试Demo
编写测试用例有一些基本原则,我们一起回顾一下:
每个测试用例只关注一个问题,不要写大而全的测试用例
测试用例是黑盒的
测试用例之间彼此独立,每个用例要保证自己的前置和后置完备
测试用例要对产品代码非入侵
…
根据基本原则,我们不要在一个测试函数的多个测试用例之间共享mock控制器
使用参数匹配器
有时,您不关心调用mock的特定参数。使用 GoMock,可以预期参数具有固定值(通过指定预期调用中的值),或者可以预期它与谓词匹配,称为 匹配器。匹配器用于表示模拟方法的预期参数范围。以下匹配器在GoMock中预定义 :
gomock.Any():匹配任何值(任何类型)
gomock.Eq(x):使用反射来匹配是值DeepEqual 到 x
gomock.Nil(): 火柴 nil
gomock.Not(m):( m 匹配器在哪里 )匹配匹配器不匹配的值 m
gomock.Not(x)(式中, x 是 不 一个Matcher)匹配的值不 DeepEqual 至 x
示例:
如果我们不关心第一个参数的值 Do,我们可以写:
mockDoer.EXPECT().DoSomething(gomock.Any(), “Hello GoMock”)
GoMock 自动将实际的参数转换 Matcher 为 Eq 匹配器,因此上述调用等效于:
mockDoer.EXPECT().DoSomething(gomock.Any(), gomock.Eq(“Hello GoMock”))
您可以通过实现gomock.Matcher 界面来定义自己的匹配器 :
//位置:gomock/matchers.go
type Matcher interface {
Matches(x interface{}) bool
String() string
}
该 Matches 方法是实际匹配发生的地方,同时 String 用于为失败的测试生成人类可读的输出。例如,检查参数类型的匹配器可以实现如下:
//位置:match/oftype.go
package match
import (
“reflect”
“github.com/golang/mock/gomock”
)
type ofType struct{ t string }
func OfType(t string) gomock.Matcher {
return &ofType{t}
}
func (o *ofType) Matches(x interface{}) bool {
return reflect.TypeOf(x).String() == o.t
}
func (o *ofType) String() string {
return “is of type “ + o.t
}
我们可是使用自定义的matcher如下:
// Expect Do to be called once with 123 and any string as parameters, and return nil from the mocked call.
mockDoer.EXPECT().
DoSomething(123, match.OfType(“string”)).
Return(nil).
Times(1)
请注意,在Go中,我们必须 在一系列链式调用中将点放在每一行的 末尾
调用对象的顺序通常很重要。 GoMock 提供了一种断言一个调用必须在另一个调用之后发生的.After 方法,即 方法。例如,
callFirst := mockDoer.EXPECT().DoSomething(1, “first this”)
callA := mockDoer.EXPECT().DoSomething(2, “then this”).After(callFirst)
callB := mockDoer.EXPECT().DoSomething(2, “or this”).After(callFirst)
GoMock 还提供了一个便利功能, gomock.InOrder 用于指定必须按照给定的确切顺序执行调用。这比.After 直接使用灵活性要差 ,但可以使您的测试对于更长的调用序列更具可读性:
gomock.InOrder(
mockDoer.EXPECT().DoSomething(1, “first this”),
mockDoer.EXPECT().DoSomething(2, “then this”),
mockDoer.EXPECT().DoSomething(3, “then this”),
mockDoer.EXPECT().DoSomething(4, “finally this”),
)
指定模拟操作
模拟对象与实际实现的不同之处在于它们不实现任何行为 - 它们所做的只是在适当的时刻提供预设响应并记录其调用。但是,有时你需要你的mock才能做更多的事情。在这里, GoMock的 Do 行动派上用场。任何调用都可以通过调用一个动作进行修饰, .Do 每当调用匹配时,都会执行一个函数:
mockDoer.EXPECT().
DoSomething(gomock.Any(), gomock.Any()).
Return(nil).
Do(func(x int, y string) {
fmt.Println(“Called with x =”,x,”and y =”, y)
})
关于调用参数的复杂断言可以写在 Do 操作中。例如,如果DoSomething第一个(int)参数 应小于或等于second(string)参数的长度,我们可以编写:
mockDoer.EXPECT().
DoSomething(gomock.Any(), gomock.Any()).
Return(nil).
Do(func(x int, y string) {
if x > len(y) {
t.Fail()
}
})