Go实用mock工具

单元测试一般不允许有任何外部依赖(文件依赖/网络依赖/数据库依赖等),不会(or不能)在测试代码中去真正 连接数据库/调用api等。这些外部依赖在执行测试时需被模拟(mock/stub)。在测试时,使用模拟的对象来模拟真实依赖下的各种行为。如何运用mock/stub来模拟系统真实行为,算是单元测试道路上的一只拦路虎

Mock(模拟)和Stub(桩)是在测试过程中,模拟外部依赖行为的两种常用的技术手段。 通过Mock和Stub我们不仅可以让测试环境没有外部依赖,而且还可以模拟一些异常行为,如数据库服务不可用,没有文件的访问权限等等。
搞定Go单元测试(一)——基础原理利用mock/stub 技术破除外部依赖

单元测试中只是针对单个函数的测试,关注其内部的逻辑,对于网络/数据库访问等,需要通过相应的手段进行 mock


gomock基础用法


gomock 是Go官方开源的golang测试框架:“GoMock is a mocking framework for the Go programming language”。

通过mockgen命令生成包含mock对象的.go文件,其生成的mock对象具备mock+stub的强大功能

安装:

go install github.com/golang/mock/mockgen@v1.6.0

**mockgen:**

mockgen has two modes of operation: source and reflect.

Source mode generates mock interfaces from a source file.
It is enabled by using the -source flag. Other flags that
may be useful in this mode are -imports and -aux_files.
Example:
mockgen -source=foo.go [other options]

Reflect mode generates mock interfaces by building a program
that uses reflection to understand interfaces. It is enabled
by passing two non-flag arguments: an import path, and a
comma-separated list of symbols.
Example:
mockgen database/sql/driver Conn,Driver

-aux_files string
(source mode) Comma-separated pkg=path pairs of auxiliary Go source files.
-build_flags string
(reflect mode) Additional flags for go build.
-copyright_file string
Copyright file used to add copyright header
-debug_parser
Print out parser results only.
-destination string
Output file; defaults to stdout.
-exec_only string
(reflect mode) If set, execute this reflection program.
-imports string
(source mode) Comma-separated name=path pairs of explicit imports to use.
-mock_names string
Comma-separated interfaceName=mockName pairs of explicit mock names to use. Mock names default to ‘Mock’+ interfaceName suffix.
-package string
Package of the generated code; defaults to the package of the input with a ‘mock_’ prefix.
-prog_only
(reflect mode) Only generate the reflection program; write it to stdout and exit.
-self_package string
The full package import path for the generated code. The purpose of this flag is to prevent import cycles in the generated code by trying to include its own package. This can happen if the mock’s package is set to one of its inputs (usually the main one) and the output is stdio so mockgen cannot detect the final output package. Setting this flag will then tell mockgen which import to exclude.
-source string
(source mode) Input Go source file; enables source mode.
-version
Print version.
-write_package_comment
Writes package documentation comment (godoc) if true. (default true)
2017/02/15 10:56:34 Expected exactly two arguments


mockgen 有两种操作模式:source 和 reflect。

源模式从源文件生成模拟接口。
它通过使用 -source 标志启用。其他标志
在这种模式下可能有用的是 -imports 和 -aux_files。
例子:
mockgen -source=foo.go [其他选项]

Reflect模式通过构建程序生成mock接口
使用反射来理解接口。它已启用
通过传递两个非标志参数:一个导入路径和一个
逗号分隔的符号列表。
例子:
mockgen 数据库/sql/driver Conn,Driver

  • aux_files 字符串
    (源模式)逗号分隔的 pkg=辅助 Go 源文件的路径对。

  • build_flags 字符串
    (反射模式)go build 的附加标志。

  • copyright_file 字符串
    用于添加版权标题的版权文件

  • debug_parser
    仅打印解析器结果。

  • 目标字符串
    输出文件;默认为标准输出。

  • exec_only 字符串
    (反射模式)如果设置,则执行此反射程序。

  • 导入字符串
    (源模式)逗号分隔的名称=要使用的显式导入的路径对。

  • mock_names 字符串
    逗号分隔的 interfaceName=mockName 要使用的显式模拟名称对。模拟名称默认为 ‘Mock’+ interfaceName 后缀。

  • 包字符串
    生成的代码包;默认为带有“mock_”前缀的输入包。

  • prog_only
    (反射模式)只生成反射程序;将其写入标准输出并退出。

  • self_package 字符串
    生成代码的完整包导入路径。此标志的目的是通过尝试包含自己的包来防止生成代码中的导入循环。如果将 mock 的包设置为其输入之一(通常是主包)并且输出是 stdio,则可能会发生这种情况,因此 mockgen 无法检测到最终输出包。设置此标志将告诉 mockgen 要排除哪个导入。

  • 源字符串
    (源模式)输入Go源文件;启用源模式。

  • 版本
    印刷版。

  • write_package_comment
    如果为真,则写入包文档注释 (godoc)。 (默认为真)


如对于user.go:

1
2
3
4
5
6
7
8
9
10
11
12
package user

// User 表示一个用户
type User struct {
Name string
}

// UserRepository 用户仓库
type UserRepository interface {
// FindOne 根据用户id查询得到一个用户或是错误信息
FindOne(id int) (*User, error)
}

执行
mockgen -source user.go -destination user_mock.go -package user


  • source:指定需要模拟(mock)的接口文件

  • destination:设置生成的mock文件名。若不设置则打印到标准输出中

  • package:设置mock文件的报名。若不设置,则为 mock_前缀加文件名

之后会在同目录下,生成一个user_mock.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
41
42
43
44
45
46
47
48
49
// Code generated by MockGen. DO NOT EDIT.
// Source: user.go

// Package user is a generated GoMock package.
package user

import (
reflect "reflect"

gomock "github.com/golang/mock/gomock"
)

// MockUserRepository is a mock of UserRepository interface.
type MockUserRepository struct {
ctrl *gomock.Controller
recorder *MockUserRepositoryMockRecorder
}

// MockUserRepositoryMockRecorder is the mock recorder for MockUserRepository.
type MockUserRepositoryMockRecorder struct {
mock *MockUserRepository
}

// NewMockUserRepository creates a new mock instance.
func NewMockUserRepository(ctrl *gomock.Controller) *MockUserRepository {
mock := &MockUserRepository{ctrl: ctrl}
mock.recorder = &MockUserRepositoryMockRecorder{mock}
return mock
}

// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockUserRepository) EXPECT() *MockUserRepositoryMockRecorder {
return m.recorder
}

// FindOne mocks base method.
func (m *MockUserRepository) FindOne(id int) (*User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FindOne", id)
ret0, _ := ret[0].(*User)
ret1, _ := ret[1].(error)
return ret0, ret1
}

// FindOne indicates an expected call of FindOne.
func (mr *MockUserRepositoryMockRecorder) FindOne(id interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOne", reflect.TypeOf((*MockUserRepository)(nil).FindOne), id)
}

在该目录下新建user_test.go文件,来写测试函数


设置函数的返回值


user_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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package user

import (
"errors"
"github.com/golang/mock/gomock"
"log"
"testing"
)

// 静态设置返回值
func TestReturn(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish() // 在go 1.14+后,如果已将*testing.T对象传入Controller,可以不用主动调用Finish()

repo := NewMockUserRepository(ctrl)

// 期望FindOne(1)返回 Alex
repo.EXPECT().FindOne(1).Return(&User{"Alex"}, nil)
// 期望FindOne(2)返回 Bob
repo.EXPECT().FindOne(2).Return(&User{Name: "Bob"}, nil)

// 期望FindOne(3)返回 user not found 的错误
repo.EXPECT().FindOne(3).Return(nil, errors.New("user not found"))

// 结果验证
log.Println(repo.FindOne(1)) // &{Alex} <nil>
log.Println(repo.FindOne(2)) // &{Bob} <nil>
log.Println(repo.FindOne(3)) // <nil> user not found
//log.Println(repo.FindOne(4))
}

// 动态设置返回值
func TestReturnDynamic(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

repo := NewMockUserRepository(ctrl)

// 常用方法之一:DoAndReturn(),动态设置返回值
repo.EXPECT().FindOne(gomock.Any()).DoAndReturn(func(i int) (*User, error) {
if i == 0 {
return nil, errors.New("user not found")
}

if i < 100 {
return &User{
Name: "小于100",
}, nil
} else {
return &User{
Name: "大于等于100",
}, nil
}

})

log.Println(repo.FindOne(126)) // &{大于等于100} <nil>
//log.Println(repo.FindOne(29))
//log.Println(repo.FindOne(0))

}


基本使用:

  1. 首先要把需要 mock 的地方,写成接口(mock 作用的是接口,因此将依赖抽象为接口,而不是直接依赖具体的类)

  2. 执行 mockgen 生成xx_mock.go代码

  3. 在单元测试中(xx_test.go),先执行 ctrl := gomock.NewController(t), 然后 defer ctrl.Finish()

  4. 使用 NewMockXXX 生成 mock 对象

  5. 调用 EXPECT() 方法,开始设置断言,对于参数,如果输入具体类型,则执行时就要传入相应类型; 如输入 gomock.Any(),则会忽略参数类型(还有 gomock.Eq, gomock.Len, gomock.All等..)

  6. 可以通过 Return 设置返回结果

  7. 可以通过 Times 设置调用次数( 另外还有 AnyTimes 不限次数,MaxTimes 最多执行次数,MinTimes 最少执行次数)

  8. 可以通过 After或InOrder 设置执行顺序


调用次数检测


user_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
func TestTimes(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

repo := NewMockUserRepository(ctrl)

// 默认期望调用一次
repo.EXPECT().FindOne(222).Return(&User{Name: "Alex"}, nil)

// 期望调用两次
repo.EXPECT().FindOne(333).Return(&User{Name: "Bob"}, nil).Times(2)

// 可以调用任意次(包括0次)
repo.EXPECT().FindOne(666).Return(nil, errors.New("出错了")).AnyTimes()

// 验证结果
log.Println(repo.FindOne(222))

log.Println(repo.FindOne(333))
log.Println(repo.FindOne(333)) // 期望FindOne(333)调用两次,如果注释此行,则测试将不通过. 报错信息为:aborting test due to missing call(s)

log.Println(repo.FindOne(666)) // 不限调用次数,注释掉这些行也能通过测试
log.Println(repo.FindOne(666)) // 不限调用次数,注释掉这些行也能通过测试
log.Println(repo.FindOne(666)) // 不限调用次数,注释掉这些行也能通过测试
log.Println(repo.FindOne(666)) // 不限调用次数,注释掉这些行也能通过测试

}

调顺序数检测


user_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
func TestOrder(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

repo := NewMockUserRepository(ctrl)

o1 := repo.EXPECT().FindOne(222).Return(&User{Name: "Alex"}, nil)
o2 := repo.EXPECT().FindOne(333).Return(&User{Name: "Bob"}, nil)
o3 := repo.EXPECT().FindOne(666).Return(nil, errors.New("出错了")).AnyTimes()

// 设置调用顺序
gomock.InOrder(o1, o2, o3)

// 验证
log.Println(repo.FindOne(222))
log.Println(repo.FindOne(333))
log.Println(repo.FindOne(666))

// 如果调整调用顺序,则测试将不能通过
//log.Println(repo.FindOne(666))
//log.Println(repo.FindOne(222))
//log.Println(repo.FindOne(333))

}

如果不按设置的顺序调用,则会报错:

doesn’t match the argument at index 0.

doesn’t have a prerequisite call satisfied:


内容参考自

搞定Go单元测试(二)—— mock框架(gomock)

使用 gomock 测试 Go 代码


更多使用技巧,参考

文档

gomock教程

“单元测试要做多细?”


Go语言使用:GoMock的基础知识


gomock其他用法


除去使用mockgen -source=xxx.go,指明从哪个源文件生成,还可以通过mockgen database/sql/driver Conn,Driver指明mock的目标是哪个包的哪些接口


另外,还可以 以 go:generate 的方式写,然后执行 go generate

generate.go:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package user

import (
"fmt"
)

//go:generate mockgen -source=./generate.go -destination mock_generate.go -package user

type Foo interface {
SayHi(sth string) error
}

type foo struct{}

func (f *foo) SayHi(sth string) error {
fmt.Printf("sth: %s\n", sth)
return nil
}

func main() {
f := foo{}
f.SayHi("hi foo")
}

执行go generate后会生成mock_generate.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
41
42
43
44
45
46
47
48
// Code generated by MockGen. DO NOT EDIT.
// Source: ./generate.go

// Package user is a generated GoMock package.
package user

import (
reflect "reflect"

gomock "github.com/golang/mock/gomock"
)

// MockFoo is a mock of Foo interface.
type MockFoo struct {
ctrl *gomock.Controller
recorder *MockFooMockRecorder
}

// MockFooMockRecorder is the mock recorder for MockFoo.
type MockFooMockRecorder struct {
mock *MockFoo
}

// NewMockFoo creates a new mock instance.
func NewMockFoo(ctrl *gomock.Controller) *MockFoo {
mock := &MockFoo{ctrl: ctrl}
mock.recorder = &MockFooMockRecorder{mock}
return mock
}

// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockFoo) EXPECT() *MockFooMockRecorder {
return m.recorder
}

// SayHi mocks base method.
func (m *MockFoo) SayHi(sth string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SayHi", sth)
ret0, _ := ret[0].(error)
return ret0
}

// SayHi indicates an expected call of SayHi.
func (mr *MockFooMockRecorder) SayHi(sth interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SayHi", reflect.TypeOf((*MockFoo)(nil).SayHi), sth)
}

编写可mock的代码


mock 作用的是接口,因此将依赖抽象为接口,而不是直接依赖具体的类。

不直接依赖实例,而是使用依赖注入降低耦合。


如果这样写,是无法mock的

1
2
3
4
5
6
7
8
func GetFromDB(key string) int {
db := NewDB()
if value, err := db.Get(key); err == nil {
return value
}

return -1
}

而如果将接口 db DB 通过参数传递到 GetFromDB(),那么就可以轻而易举地传入 Mock 对象

1
2
3
4
5
6
7
8
9
10
11
12
// db.go
type DB interface {
Get(key string) (int, error)
}

func GetFromDB(db DB, key string) int {
if value, err := db.Get(key); err == nil {
return value
}

return -1
}

参考自:

Go Mock (gomock)简明教程