安装
安装gomock软件包和mockgen代码生成工具
go get github.com/golang/mock/gomock
go github.com/golang/mock/mockgen
基本用法
1.用mockgen为要模拟的接口生成模拟。
2.在测试中创建一个市里gomock.Controller并将其传递给模拟对象构造函数以获取模拟对象。
3.调用EXPECT()为你的模拟设置他们的期望值和返回值
4.调用Finish()模拟控制器来断言模拟的期望。
5.举个简单的例子:
//位置:doer/doer.go
package doer
type Doer interface {
DoSomething(int, string) error
}
我们在模拟Doer时要的测试代码
//位置:user/user.go
package user
import "cold/stu1/doer"
type User struct {
Doer doer.Doer
}
func (u *User) Use() error {
return u.Doer.DoSomething(123, "Hello Gomock")
}
生成Doer的模拟代码:
mkdir -p mocks
mockgen -destination=mocks/mock_doer.go -package=mocks cold/stu1/doer Doer
注释:
- -destination=mocks/mock_doer.go : 将生成的模拟接口放入文件中mocks/mock_doer.go
- -package=mocks : 将生成的模拟接口放入包mocks中
- cold/stu1/doer:为此包生成模拟,注意:这里不要加入绝对路径,加入$GOPATH的相对路径,否则会报错
- Doer : 为此接口生成模拟。这个参数是必需的 - 我们需要指定接口来显式生成模拟。但是,我们 可以在此指定多个接口作为逗号分隔列表(例如 Doer1,Doer2)
这样在mocks下面就会自动生成一个mocks/mock_doer.go的文件,如下内容:
// Code generated by MockGen. DO NOT EDIT.
// Source: cold/stu1/doer (interfaces: Doer)
// Package mocks is a generated GoMock package.
package mocks
import (
gomock "github.com/golang/mock/gomock"
reflect "reflect"
)
// MockDoer is a mock of Doer interface
type MockDoer struct {
ctrl *gomock.Controller
recorder *MockDoerMockRecorder
}
// MockDoerMockRecorder is the mock recorder for MockDoer
type MockDoerMockRecorder struct {
mock *MockDoer
}
// NewMockDoer creates a new mock instance
func NewMockDoer(ctrl *gomock.Controller) *MockDoer {
mock := &MockDoer{ctrl: ctrl}
mock.recorder = &MockDoerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockDoer) EXPECT() *MockDoerMockRecorder {
return m.recorder
}
// DoSomething mocks base method
func (m *MockDoer) DoSomething(arg0 int, arg1 string) error {
ret := m.ctrl.Call(m, "DoSomething", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// DoSomething indicates an expected call of DoSomething
func (mr *MockDoerMockRecorder) DoSomething(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DoSomething", reflect.TypeOf((*MockDoer)(nil).DoSomething), arg0, arg1)
}
请注意,生成的 EXPECT() 方法在与mock方法相同的对象上定义(在本例中 DoSomething) - 避免此处的名称冲突可能是非标准全大写名称的原因
接下来,我们 在测试中定义一个 模拟控制器。模拟控制器负责跟踪和断言其关联模拟对象的期望。
我们可以通过将t 类型 的值传递*testing.T 给它的构造函数来获得模拟控制器 ,然后使用它来构造Doer 接口的模拟 。我们还有 defer 它的 Finish 方法。
假设我们要断言 mockerDoer的 Do 方法将被调用 一次,以 123 和 "Hello GoMock" 为参数,将返回 nil。
要做到这一点,我们就可以调用 EXPECT() 上 mockDoer 建立其期望在我们的测试。调用 EXPECT()返回一个对象(称为模拟 记录器),提供与真实对象相同名称的方法。
调用模拟记录器上的方法之一指定具有给定参数的预期调用。然后,您可以将其他属性链接到呼叫上,例如:
返回值(通过 .Return(...))
此呼叫预计发生的次数(通过 .Times(number)或通过 .MaxTimes(number) 和 .MinTimes(number))
代码如下:
func TestUse(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
mockDoer := mocks.NewMockDoer(mockCtrl)
testUser := &user.User{Doer: mockDoer}
mockDoer.EXPECT().DoSomething(123, "Hello Gomock").Return(nil).Times(1)
testUser.Use()
}
最后,我们来运行一下我们的测试:
$ go test -v cold/stu1/user
=== RUN TestUse
--- PASS: TestUse (0.00s)
=== RUN TestUseReturnsErrorFromDo
--- PASS: TestUseReturnsErrorFromDo (0.00s)
PASS
ok cold/stu1/user 0.002s
我们可能还想声明Use 方法返回的值确实是由DoSomething返回的值。我们可以编写另一个测试,创建一个虚拟错误,然后将其指定为返回值
func TestUseReturnsErrorFromDo(t *testing.T) {
mockCtr := gomock.NewController(t)
defer mockCtr.Finish()
dummyError := errors.New("dummy error")
mockDoer := mocks.NewMockDoer(mockCtr)
testUser := &user.User{Doer: mockDoer}
mockDoer.EXPECT().DoSomething(123, "Hello Gomock").Return(dummyError).Times(1)
err := testUser.Use()
if err != dummyError {
t.Fail()
}
}
使用GoMock 的 go:generate
mockgen
当存在大量要模拟的接口/包时,单独运行 每个包和接口是很麻烦的。为了缓解此问题, mockgen
可以将命令放在特殊 go:generate
注释中。
在我们的示例中,我们可以在我们go:generate 的package 声明 下方添加 注释 doer.go:
package doer
//go:generate mockgen -destination=../mocks/mock_doer.go -package=mocks cold/stu1/doer Doer
type Doer interface {
DoSomething(int, string) error
}
注意:由于当前的包doer的工作路径是doer,所以在destination要写成 ../mocks/,而不是直接是mocks,执行命令:
go generate ./...
那么我们现在可以运行所有有go:generate ./... 注释的mocks,注意:注释符 //与go:generate之间没有空格。
关于在何处放置go:generate 注释以及要包括哪些接口的合理策略如下:
- go:generate 每个文件一条注释,包含要模拟的接口
- 一调用mockgen,就会为所有的接口生成mocks
- 将模拟放入包中 mocks 并将文件的模拟 X.go 写入 mocks/mock_X.go。
这样, mockgen 调用接近实际接口,同时避免了每个接口的单独调用和目标文件的开销。
使用参数匹配器
有时,您不关心调用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()
}
})