背景
最近在研究用Go写一个自己的解释型语言,有一本书叫《Writing An Interpreter In Go》, 作者在讲解如何编写解释器的时候,都是从写一个_test.go
开始的,也就是说作者习惯于先写单元测试,以测试驱动开发,其实这是一个非常好的习惯,不过,作者在写_test.go
文件的时候,都是先假设这个结构体、函数已经存在了,并且没有把关键的对象抽象成接口,因此,作者在运行go test
的时候,是无法完成测试的,因为连编译都过不了,必须一边完善代码,一边重复运行go test
,一直到完成开发。
基于这种开发模式下,其实我更期望能有一个Mock实现,写测试代码的时候畅通无阻,即使是没有实现,也能把各个测试用例覆盖到,当真实的实现完成后,我们只需要把mock实现替换成真实的实现就好了。
这么做还带来另一个好处,如果公司有SDET岗位,则可以直接让测试人员编写单元测试,开发任务和测试任务可以并行。
gomock 框架
昨天闲着没事逛了逛 https://github.com/golang, 发现了一个非常有意思的框架: gomock, 官方的描述是,这是一个mocking framework, 在使用上也很简单,大致的步骤如下:
1、定义一个待实现的接口.
1 | type MyInterface interface { |
2、使用mockgen
生成mock代码.
3、测试:
1 | func TestMyThing(t *testing.T) { |
看了这个步骤,想必大家应该猜到了,mocking framework 使用的是根据你的接口定义来自动生成一个Mock实现,我们还可以往这个实现里注入数据。
期望接口调用参数
我们可以通过 .EXPECT()
为这个mock对象注入期望值
1 | mockObj.EXPECT().SomeMethod(4, "blah") |
- 测试通过:
1 | mockObj.SomeMethod(4, "blah") |
- 测试不通过:
1 | mockObj.SomeMethod(5, "bldah") // 此处调用时直接会抛出错误 |
对于参数类型的期望,在调用这个Mock函数的时候会直接抛错异常
期望返回值
.EXPECT()
同样可以为某个函数注入返回值
1 | mockObj.EXPECT().GetSomething().Return("haha") |
- 测试通过:
1 | if "haha" == mockObj.GetSomething() { |
- 测试不通过:
1 | if "haha" == mockObj.GetSomething() { |
功能强的还不止这个,如果我们在测试一个循环,希望的是每次调用 GetSomething()
都返回不同的值,该怎么办?
答案很简单,依次调用
1 | gomock.InOrder( |
接下来,让我们来实战一下吧。
gomock 实战
我们以《Writing An Interpreter In Go》这本书中的 monkey
语言的 lexer
作为例子
我们看一下 monkey
的目录结构:
1 | monkey> tree . |
Lexer和Token
1 | type Lexer interface { |
lexer的功能很简单,每次调用NextToken()
,都是返回下一个Token
Token
的结构1
2
3
4
5
6type TokenType string
type Token struct {
Type TokenType // 类型
Literal string // 内容
}
比如下面的go
语句
1 | var a = 1 |
lexter 在调用三次NextToken()
后会得到三个Token, 依次是:
1 | Token{VAR, var} |
测试思路
其实测试方法就是:给定一段代码,用Lexer解析后,能得到指定顺序的Token,而gomock
是完全可以实现的。
使用gomock
安装gomock
1 | go get github.com/golang/mock/gomock |
生成mock代码
1 | mockgen -source lexer.go -destination mock_lexer/mock_lexer.go |
编写lexer_test.go
测试数据
input: 输入的语句
tokens: 期望的Token
1 | func getTestData() (input string, tokens []token.Token) { |
生成一个真实的MonkeyLexer实例
当然,这里我们没有实现,所以返回是nil
1 | func newMonkeyLexer(input string, excepts []token.Token, t *testing.T) (l Lexer, deferFN func()) { |
构建MockLexer实例
由于没写完真正的lexer, 那么我们就开始Mock吧
1 | func newMockLexer(input string, excepts []token.Token, t *testing.T) (l Lexer, deferFN func()) { |
为了方便在mock实例和真实实例之间进行切换,我们可以通过环境变量来控制当前的测试实例是什么,如果要使用mock进行测试,我们只需要在运行 go test
前执行:
1 | > export GO_MOCK_TEST=1 |
或
1 | > GO_MOCK_TEST=1 go test -v |
1 | func newLexer(input string, excepts []token.Token, t *testing.T) (l Lexer, deferFN func()) { |
以下是真正的测试代码,在没真实实现monkey lexer
的情况下,我们可以写测试代码了,而且如果运行 go test -v
也是能通过的。
1 | func TestNextToken(t *testing.T) { |
测试
使用mock实例进行测试
1 | monkey> cd lexer |
使用真实实例测试
1 | func newMonkeyLexer(input string, excepts []token.Token, t *testing.T) (l Lexer, deferFN func()) { |
我们将测试数据修改一下,假设 ten=666
, 但不修改期望值,让测试报错
1 | input = ` |
再次运行测试
1 | monkey> cd lexer |
这里就报错了,说明我们的真实实例实现得有问题,需要修复这个BUG
gomock
的使用到这里就结束了,除了上面介绍到的一些功能,gomock
还有很多其他丰富的方法,大家可以去 GoDoc 获取更详细的接口信息。
欢迎关注我的微信公众账号: DeepIn-z