## 使用Mockgen包 前面的小节我们使用了自行模拟的方式。当你需要面对很多的接口时,这么干会变得极为麻烦且极易发生错误。这是自动化测试的意义所在。本节我们使用 github.com/golang/mock/gomock ,该库提供了一组模拟对象,可以与接口测试结合使用。 ### 实践 1.获取第三方库: ``` go get github.com/golang/mock/ ``` 2. 建立 interface.go: ``` package mockgen type GetSetter interface { Set(key, val string) error Get(key string) (string, error) } ``` 3. 运行命令行建立 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) } ``` 4. 建立 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 } ``` 5. 建立 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) } } ``` 6. 建立 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文件中更容易。 * * * * 学识浅薄,错误在所难免。欢迎在群中就本书提出修改意见,以飨后来者,长风拜谢。 Golang中国(211938256) beego实战(258969317) Go实践(386056972)