前面的小节我们使用了自行模拟的方式。当你需要面对很多的接口时,这么干会变得极为麻烦且极易发生错误。这是自动化测试的意义所在。本节我们使用 github.com/golang/mock/gomock ,该库提供了一组模拟对象,可以与接口测试结合使用。
实践
获取第三方库:
go get github.com/golang/mock/
建立 interface.go:
package mockgen
type GetSetter interface {
Set(key, val string) error
Get(key string) (string, error)
}
运行命令行建立 mocks.go:
mockgen -destination internal/mocks.go -package internal
github.com/agtorre/go-cookbook/chapter8/mockgen GetSetter
// Automatically generated by MockGen. DO NOT EDIT!
// Source: github.com/agtorre/go-cookbook/chapter8/mockgen (interfaces: GetSetter)
package internal
import (
gomock "github.com/golang/mock/gomock"
)
// Mock of GetSetter interface
type MockGetSetter struct {
ctrl *gomock.Controller
recorder *_MockGetSetterRecorder
}
// Recorder for MockGetSetter (not exported)
type _MockGetSetterRecorder struct {
mock *MockGetSetter
}
func NewMockGetSetter(ctrl *gomock.Controller) *MockGetSetter {
mock := &MockGetSetter{ctrl: ctrl}
mock.recorder = &_MockGetSetterRecorder{mock}
return mock
}
func (_m *MockGetSetter) EXPECT() *_MockGetSetterRecorder {
return _m.recorder
}
func (_m *MockGetSetter) Get(_param0 string) (string, error) {
ret := _m.ctrl.Call(_m, "Get", _param0)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
func (_mr *_MockGetSetterRecorder) Get(arg0 interface{}) *gomock.Call {
return _mr.mock.ctrl.RecordCall(_mr.mock, "Get", arg0)
}
func (_m *MockGetSetter) Set(_param0 string, _param1 string) error {
ret := _m.ctrl.Call(_m, "Set", _param0, _param1)
ret0, _ := ret[0].(error)
return ret0
}
func (_mr *_MockGetSetterRecorder) Set(arg0, arg1 interface{}) *gomock.Call {
return _mr.mock.ctrl.RecordCall(_mr.mock, "Set", arg0, arg1)
}
建立 exec.go:
package mockgen
// Controller 这个结构体演示了一种初始化接口的方式
type Controller struct {
GetSetter
}
// GetThenSet 检查值是否已设置。如果没有设置就将其设置
func (c *Controller) GetThenSet(key, value string) error {
val, err := c.Get(key)
if err != nil {
return err
}
if val != value {
return c.Set(key, value)
}
return nil
}
建立 interface_test.go:
package mockgen
import (
"errors"
"testing"
"github.com/agtorre/go-cookbook/chapter8/mockgen/internal"
"github.com/golang/mock/gomock"
)
func TestExample(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockGetSetter := internal.NewMockGetSetter(ctrl)
var k string
mockGetSetter.EXPECT().Get("we can put anything here!").Do(func(key string) {
k = key
}).Return("", nil)
customError := errors.New("failed this time")
mockGetSetter.EXPECT().Get(gomock.Any()).Return("", customError)
if _, err := mockGetSetter.Get("we can put anything here!"); err != nil {
t.Errorf("got %#v; want %#v", err, nil)
}
if k != "we can put anything here!" {
t.Errorf("bad key")
}
if _, err := mockGetSetter.Get("key"); err == nil {
t.Errorf("got %#v; want %#v", err, customError)
}
}
建立 exec_test.go:
package mockgen
import (
"errors"
"testing"
"github.com/agtorre/go-cookbook/chapter8/mockgen/internal"
"github.com/golang/mock/gomock"
)
func TestController_Set(t *testing.T) {
tests := []struct {
name string
getReturnVal string
getReturnErr error
setReturnErr error
wantErr bool
}{
{"get error", "value", errors.New("failed"), nil, true},
{"value match", "value", nil, nil, false},
{"no errors", "not set", nil, nil, false},
{"set error", "not set", nil, errors.New("failed"), true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockGetSetter := internal.NewMockGetSetter(ctrl)
mockGetSetter.EXPECT().Get("key").AnyTimes().Return(tt.getReturnVal, tt.getReturnErr)
mockGetSetter.EXPECT().Set("key", gomock.Any()).AnyTimes().Return(tt.setReturnErr)
c := &Controller{
GetSetter: mockGetSetter,
}
if err := c.GetThenSet("key", "value"); (err != nil) != tt.wantErr {
t.Errorf("Controller.Set() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
说明
生成的模拟对象允许测试预定的参数,调用函数的次数以及返回的内容,并且允许我们设置其他工作流程。interface_test.go文件展示了在线调用它们时使用模拟对象的一些示例。 通常,测试看起来更像exec_test.go,我们希望拦截由实际代码执行的接口函数调用,并在测试时更改它们的行为。
exec_test.go文件还展示了如何在表驱动的测试环境中使用模拟对象。Any()函数意味着模拟函数可以被调用零次或多次,这对于代码提前终止的情况非常有用。
示例演示的最后一个技巧是将模拟对象粘贴到内部包中。当需要模拟在自己之外的包中声明的函数时,这非常有用。 这允许在非test.go文件中定义这些方法,但不允许将它们导出到库的情况。通常,使用与当前编写的测试相同的包名称将模拟对象粘贴到test.go文件中更容易。
最后编辑: kuteng 文档更新时间: 2021-01-03 15:03 作者:kuteng