简单总结下 Golang Mock 测试中的四大宗师:gomock、monkey、httptest、sqlmock
gomock
特点:强依赖interface进行打桩
- github.com/golang/mock(不再更新)
- go.uber.org/mock
GoMock 的使用通常遵循如下四个基本步骤:
- 使用
mockgen
为你想要mock
的接口生成一个mock
。 - 在你的测试代码中,创建一个
gomock.Controller
实例并把它作为参数传递给mock
对象的构造函数来创建一个mock
对象。 - 调用
EXPECT()
为你的mock
对象设置各种期望和返回值。 - 调用
mock
控制器的Finish()
以验证mock
的期望行为。
1 | package person |
注意注释中的 //
和 go:generate
之间不能有空格,这样 go generate
才可以把注释当做一条命令来处理。
关于 go:generate
注释以及注释中包含哪些接口,可以遵循如下原则:
- 每个包含需要
mock
接口的文件都添加一条go:generate
注释 - 注释中的
mockgen
命令包括该文件中所有要mock
的接口 - 将所有生成的
mock
文件全部放在mocks
包中并统一命名,即X.go文件的mock代码输出到mocks/mock_X.go
中
通过以上方式 mockgen
命令既能和实际要 mock
的接口放在一起,又能避免针对每个接口单独调用 mockgen
和维护输出文件的麻烦。
测试
1 | mockgen -source=./person/male.go -destination=../mocks/mock_male.go -package=mocks |
monkey
特点:利用 猴子补丁(monkey patching),替换改写方法,成员方法、全局变量
- https://github.com/bouk/monkey(不再更新)
- https://github.com/agiledragon/gomonkey(已经支持arm和全部go版本)
- 不支持内联函数,在测试的时候需要通过
go test -gcflags=all=-l
关闭内联优化。 - 不是线程安全的,所以不要把它用到并发的单元测试中。
优化版 monkey
https://github.com/go-kiss/monkey
对 Bouke 的项目做了优化,不同协程可以独立 patch
同一个函数而互不影响,从而可以并发运行单元测试。
工作原理请参考系列文章Go语言实现猴子补丁。
注意事项:
- 同样需要关闭 Go 语言的内联优化才能生效。
- 需要在运行的时候修改内存代码段,因而无法在一些对安全性要求比较高的系统上工作。
- 不应该用于生产系统,但用来 mock 测试代码还是没有问题的。
- 目前仅支持 amd64 指令架构,支持 linux/macos/windows 平台。
httptest
Go 本身提供了一个非常实用的用于 HTTP 测试的程序包 —— httptest
。该包内部 Server
被描述为:
Server 是一个 HTTP 服务器,监听本地环回接口上的系统选择的端口,用于端到端 HTTP 测试。
具体使用可参考 httptest
sqlmock
特点: 通过中间件底层链接进行 mock
- github.com/DATA-DOG/go-sqlmock
- github.com/go-redis/redismock
- github.com/alicebob/miniredis/v2
goconvey
github.com/smartystreets/goconvey
,主要特点:
- 集成于go test
- 可读的彩色控制台输出
- 全自动网页用户界面
- 大量的回归测试
- 测试代码生成器
安装并启动 gocovey
后,默认在本机 8080
端口提供 WebUI 界面,十分清晰地展现当前项目的单元测试数据。
与其他 Go 测试工具相比的所有功能的综合表:
✓ = Has it
~ = Kind of has it
? = Not sure yet
Feature | GoConvey | Native go test |
Testify | goblin | PrettyTest | Ginkgo |
---|---|---|---|---|---|---|
Uses go test |
✓ | ✓ | ✓ | ✓ | ✓ | ~ |
Web UI | ✓ | |||||
Web UI reports traditional Go tests | ✓ | |||||
Open files in Sublime Text | ✓ | |||||
Auto-test | ✓ | ? | ? | ✓ | ||
Test code generator | ✓ | ~ | ||||
Custom assertions/matchers | ✓ | ✓ | ? | ~ | ✓ | ✓ |
Optional verbose output | ✓ | ✓ | ? | ? | ✓ | ✓ |
Colorized console output | ✓ | ? | ✓ | ✓ | ✓ | |
Non-IT-readable output | ✓ | ✓ | ✓ | ✓ | ||
Randomized test execution | ✓ | |||||
Coverage report | ✓ | ✓ | ||||
Skip test blocks/assertions | ✓ | ✓ | ? | ✓ | ? | ✓ |
Flexible DSL | ✓ | ✓ | ||||
Supports BDD/TDD/Acceptance | ✓ | ✓ | ✓ | |||
Assertions | ||||||
- Equal | ✓ | ✓ | ✓ | ✓ | ✓ | |
- DeepEqual | ✓ | ✓ | ✓ | ✓ | ||
- True | ✓ | ✓ | ✓ | ✓ | ||
- False | ✓ | ✓ | ✓ | ✓ | ||
- Nil | ✓ | ✓ | ✓ | ✓ | ||
- Empty | ✓ | ✓ | ✓ | |||
- (a whole bunch more) | ✓ | ✓ | ✓ | ✓ |
Some table data compiled from Go Test It
有关断言、编写测试、执行等的详细信息,请参阅 文档索引
有关 GoConvey
执行模型的一些重要问题,例如:
- 如何定义在每次测试之前运行的
Setup
方法? - 为什么我的嵌套测试不按顺序运行?
令人惊讶的是,这些问题是相关的。例如,考虑伪代码:
1 | Convey A |
我最初认为会执行为
A1B2C3
,换句话说,顺序执行。大家都知道,这实际上是先执行A1B2
,然后执行A1C3
。一旦我意识到这一点,我实际上意识到了 goconvey 的强大功能,因为它允许您使用log(n)
语句进行两次write n
测试。这种 树状 行为测试消除了很多重复的设置代码,并且更容易阅读完整性(相对于单元测试的页面),同时仍然允许非常好的隔离测试(对于树中的每个分支)。
在上面的伪代码中, Convey A
充当了 Convey B
和 Convey C
的 “Setup” 方法并且分别为每个方法运行。
下面是一个更复杂的示例:
1 | Convey A |
你能猜出输出是什么吗?
A1B2Q9A1C3
是正确的答案。
总结
mock 三种主流方案对比:
评估项 | gomock | monkey | sqlmock |
---|---|---|---|
代码侵入性 | 强依赖interface,影响编码规范 | 无侵入 | 需要支持动态替换中间件实例 |
成本 | 高 | 低 | 一般 |
灵活性 | 每次增加方法都需要修改mock实现 | 按需mock | 工具受限、场景受限 |
三种不同mock工具适用的场景:
- gomock:适合于 业务代码分层互相依赖已经使用 interface 情况,或对第三方依赖是 interface 情况下使用
- monkey:万金油无论是对方法、变量都可以mock,甚至官方函数都行,但不支持并行测试,改写方法是全局生效的
- sqlmock:不太适合func的单测会增加单测范围以及反调用直觉,比较适合于一一个服务的全流程单测