go test 下篇
前言
go test 上篇 给大家介绍了golang自带的测试框架,包括单元测试和性能测试。但是在实际生产中测试经常会遇到一些网络或者依赖的第三方系统接口,运行测试用例的时候希望忽略这些接口的实际依赖,聚焦在具体业务逻辑代码,这就需要模拟这些接口的行为,也就是我今天介绍给大家的golang/mock,一个golang的mock框架。
演示环境
$ uname -a Darwin 18.6.0 Darwin Kernel Version 18.6.0: Thu Apr 25 23:16:27 PDT 2019; root:xnu-4903.261.4~2/RELEASE_X86_64 x86_64 $ go version go version go1.12.4 darwin/amd64
安装
go get github.com/golang/mock/gomock go install github.com/golang/mock/mockgen
用法
1.定义我们需要mock的接口。如:
type MyInterface interface { SomeMethod(x int64, y string) }
2.使用mockgen命令生成接口的mock文件。
mockgen -package example_test -destination example_mock.go
3.在测试中使用mock接口:
func TestMyThing(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() mockObj := something.NewMockMyInterface(mockCtrl) mockObj.EXPECT().SomeMethod(4, "blah") // pass mockObj to a real object and play with it. }
接口说明
以官方提供的https://github.com/golang/mock/blob/master/sample/user_test.go文件作为示例说明:
1 func TestRemember(t *testing.T) { 2 ctrl := gomock.NewController(t) 3 defer ctrl.Finish() 4 5 mockIndex := mock_user.NewMockIndex(ctrl) 6 mockIndex.EXPECT().Put("a", 1) // literals work 7 mockIndex.EXPECT().Put("b", gomock.Eq(2)) // matchers work too 8 9 // NillableRet returns error. Not declaring it should result in a nil return. 10 mockIndex.EXPECT().NillableRet() 11 // Calls that returns something assignable to the return type. 12 boolc := make(chan bool) 13 // In this case, "chan bool" is assignable to "chan<- bool". 14 mockIndex.EXPECT().ConcreteRet().Return(boolc) 15 // In this case, nil is assignable to "chan<- bool". 16 mockIndex.EXPECT().ConcreteRet().Return(nil) 17 18 // Should be able to place expectations on variadic methods. 19 mockIndex.EXPECT().Ellip("%d", 0, 1, 1, 2, 3) // direct args 20 tri := []interface{}{1, 3, 6, 10, 15} 21 mockIndex.EXPECT().Ellip("%d", tri...) // args from slice 22 mockIndex.EXPECT().EllipOnly(gomock.Eq("arg")) 23 24 user.Remember(mockIndex, []string{"a", "b"}, []interface{}{1, 2}) 25 // Check the ConcreteRet calls. 26 if c := mockIndex.ConcreteRet(); c != boolc { 27 t.Errorf("ConcreteRet: got %v, want %v", c, boolc) 28 } 29 if c := mockIndex.ConcreteRet(); c != nil { 30 t.Errorf("ConcreteRet: got %v, want nil", c) 31 } 32 33 // Try one with an action. 34 calledString := "" 35 mockIndex.EXPECT().Put(gomock.Any(), gomock.Any()).Do(func(key string, _ interface{}) { 36 calledString = key 37 }) 38 mockIndex.EXPECT().NillableRet() 39 user.Remember(mockIndex, []string{"blah"}, []interface{}{7}) 40 if calledString != "blah" { 41 t.Fatalf(`Uh oh. %q != "blah"`, calledString) 42 } 43 44 // Use Do with a nil arg. 45 mockIndex.EXPECT().Put("nil-key", gomock.Any()).Do(func(key string, value interface{}) { 46 if value != nil { 47 t.Errorf("Put did not pass through nil; got %v", value) 48 } 49 }) 50 mockIndex.EXPECT().NillableRet() 51 user.Remember(mockIndex, []string{"nil-key"}, []interface{}{nil}) 52 }
- EXPECT 表示期望在后续的测试代码中会用到,且一定要用到,否则会报错。例如第6行的Put("a", 1)方法会在第24行的Remeber函数里面调用。
- Return 表示mock接口的返回值,例如第14行的ConcreteRet()函数返回boolc。
- Do 表示当匹配到对应的函数时执行对应的行为。例如第35行,当匹配到put(gomock.Any(), gomock.Any())时执行func(key string, _ interface{}),如果函数需要返回值用DoAndReturn函数。
- Any 表示构造一个一直会match的matcher。
上述示例使用了Index接口的mock方法。第6,7,10,14,16,19,21,22定义的EXPECT行为会在第24行的Remeber函数中被调用:
user.Remember(mockIndex, []string{"a", "b"}, []interface{}{1, 2})
1 func Remember(index Index, keys []string, values []interface{}) { 2 for i, k := range keys { 3 index.Put(k, values[i]) 4 } 5 err := index.NillableRet() 6 if err != nil { 7 log.Fatalf("Woah! %v", err) 8 } 9 if len(keys) > 0 && keys[0] == "a" { 10 index.Ellip("%d", 0, 1, 1, 2, 3) 11 index.Ellip("%d", 1, 3, 6, 10, 15) 12 index.EllipOnly("arg") 13 } 14 }
mock接口文件完成后运行测试:
$ git clone https://github.com/golang/mock Cloning into 'mock'... remote: Enumerating objects: 4, done. remote: Counting objects: 100% (4/4), done. remote: Compressing objects: 100% (4/4), done. remote: Total 1568 (delta 0), reused 2 (delta 0), pack-reused 1564 Receiving objects: 100% (1568/1568), 450.07 KiB | 354.00 KiB/s, done. Resolving deltas: 100% (807/807), done. $ cd mock/sample/ $ go test -v === RUN TestRemember --- PASS: TestRemember (0.00s) === RUN TestVariadicFunction --- PASS: TestVariadicFunction (0.00s) === RUN TestGrabPointer --- PASS: TestGrabPointer (0.00s) === RUN TestEmbeddedInterface --- PASS: TestEmbeddedInterface (0.00s) === RUN TestExpectTrueNil --- PASS: TestExpectTrueNil (0.00s) PASS ok github.com/golang/mock/sample 0.017s
在实际生产中经常将需要mock的接口对象定义为一个全局变量,然后在测试用例中用mock对象替换这个对象,替换的方法可以直接替换,也可以用goStub第三方优雅替换。
1 var configFile = "config.json" 2 3 func GetConfig() ([]byte, error) { 4 return ioutil.ReadFile(configFile) 5 } 6 7 // Test code 8 stubs := gostub.Stub(&configFile, "/tmp/test.config") 9 10 data, err := GetConfig() 11 // data will now return contents of the /tmp/test.config file
总结
文章具体介绍了gomock库的使用场景和具体用法,作为go test官方测试框架的一个补充。gomock在生产代码中会被经常用到,当然也有其他的golang mock第三方开源库,例如testify。具体的选择需要根据大家的需求具体分析。