GoMock
是一个Go框架。它与内置的测试包整合得很好,并在单元测试时提供了灵活性。正如我们所知,对具有外部资源(数据库、网络和文件)或依赖关系的代码进行单元测试总是很麻烦。
安装
为了使用GoMock,我们需要安装gomock
包github.com/golang/mock/gomock
和mockgen
代码生成工具github.com/golang/mock/mockgen
。使用这个命令行:
go get github.com/golang/mock/gomock go get github.com/golang/mock/mockgen
GoMock的使用遵循四个基本步骤:
- 使用
mockgen
为你想模拟的接口生成一个模拟对象。 - 在测试部分,创建一个
gomock.Controller
的实例,并把它传递给你的mock
对象的构造函数以获得一个mock
对象。 - 在mock上调用
EXPECT()
来设置期望值和返回值。 - 在模拟控制器上调用
Finish()
来断言模拟对象的期望。
开始
让我们创建一个这样的文件夹
gomock_test ├── doer │ └── doer.go ├── mocks │ └── mock_doer.go └── user ├── user.go └── user_test.go
doer/doer.go
package doer type Doer interface { DoSomething(int, string) error SaySomething(string) string }
那么这里是我们在模拟Doer接口时要测试的代码。
user/user.go
package user import "gomock_test/doer" const ( filtered = "OK" unfiltered = "spam" Nice = "nice" Bad = "bad" ) type User struct { // struct while mocking the doer interface Doer doer.Doer } // method Use using it func (u *User) Use() error { return u.Doer.DoSomething(123, "Hello GoMock") } func (u *User) SaySomething(num int) string { if num == 3 { return u.Doer.SaySomething(unfiltered) } return u.Doer.SaySomething(filtered) } type Student struct{} func (s *Student) DoSomething(_ int, _ string) error { panic("not implemented") // TODO: Implement } func (s *Student) SaySomething(kata string) string { if kata == filtered { return Nice } return Bad }
我们将把Doer
的模拟放在一个包mocks
中。我们首先创建一个包含我们的模拟实现的目录mocks
,然后在doer
包上运行mockgen
:
mockgen -destination=../mocks/mock_doer.go -package=mocks gomock_test/doer Doer
NOTE: 在执行这步的时候,会报错:Failed to format generated source code: mocks/mock_doer.go:5:15: expected ';', found '.’ 这个时候,我们只需要将打印出来的代码复制到我们对应的文件中即可。
当有大量的接口/包需要模拟时,为每个包和接口运行mockgen是一种乌托邦。为了缓解这个问题,可以将mockgen
命令与go:generate
放在一起。
go:generate mockgen -destination=../mocks/mock_doer.go -package=mocks gomock_test/doer Doer
我们必须自己创建目录模拟,因为GoMock不会为我们这样做,而是会以错误退出。
- destination=.../mocks/mock_doer.go : 把生成的mocks放在这个路径下。
- -package=mocks : 把生成的mocks放在这个包里
- gomock_test/doer : 为这个包生成mocks。
- Doer : 为这个接口生成mocks(必填),因为我们需要指定哪个接口来生成mocks。(如果需要的话,可以用逗号分隔的列表来指定多个接口。例如,Doer1, Doer2)
因为我们对mockgen
的调用在我们的项目中放置了一个文件mocks/mock_doer.go
。这就是这样一个生成的mock
实现的样子:
// Code generated by MockGen. DO NOT EDIT. // Source: github.com/timliudream/go-test/gomock_test/doer (interfaces: Doer) // Package github.com/timliudream/go-test/gomock_test/mocks is a generated GoMock package. package mocks import ( gomock "github.com/golang/mock/gomock" reflect "reflect" ) // MockDoer is a mock of Doer interface. type MockDoer struct { ctrl *gomock.Controller recorder *MockDoerMockRecorder } // MockDoerMockRecorder is the mock recorder for MockDoer. type MockDoerMockRecorder struct { mock *MockDoer } // NewMockDoer creates a new mock instance. func NewMockDoer(ctrl *gomock.Controller) *MockDoer { mock := &MockDoer{ctrl: ctrl} mock.recorder = &MockDoerMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. func (m *MockDoer) EXPECT() *MockDoerMockRecorder { return m.recorder } // DoSomething mocks base method. func (m *MockDoer) DoSomething(arg0 int, arg1 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "DoSomething", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } // DoSomething indicates an expected call of DoSomething. func (mr *MockDoerMockRecorder) DoSomething(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DoSomething", reflect.TypeOf((*MockDoer)(nil).DoSomething), arg0, arg1) } // SaySomething mocks base method. func (m *MockDoer) SaySomething(arg0 string) string { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SaySomething", arg0) ret0, _ := ret[0].(string) return ret0 } // SaySomething indicates an expected call of SaySomething. func (mr *MockDoerMockRecorder) SaySomething(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaySomething", reflect.TypeOf((*MockDoer)(nil).SaySomething), arg0) }
接下来,我们在测试中定义一个模拟控制器。一个模拟控制器负责跟踪和断言其相关模拟对象的期望。我们可以通过传递一个*testing.T
类型的值给它的构造函数来获得一个模拟控制器,并使用它来构造一个Doer
接口的模拟对象。
//core of gomock mockCtrl := gomock.NewController(t) //used to trigger final assertions. if its ignored, mocking assertions will never fail defer mockCtrl.Finish() // create a new mock object, passing the controller instance as parameter // for a newly created mock object it will accept any input and outpuite // need to define its behavior with the method expect mockDoer := mocks.NewMockDoer(mockCtrl)
使用参数匹配器
在GoMock中,一个参数可以被期望有一个固定的值,也可以被期望与一个谓词(称为匹配器)相匹配。匹配器用于表示被模拟方法的预期参数范围。下列匹配器在Gomock中被预先定义了:
- gomock.Any() : 匹配任何值(任何类型)。
- gomock.Eq(x) : 使用反射来匹配与x深度相等的值。
- gomock.Nil() : 匹配nil
user/user_test.go
package user import ( "fmt" "github.com/golang/mock/gomock" "gomock_test/mocks" "testing" ) func TestUse(t *testing.T) { //core of gomock mockCtrl := gomock.NewController(t) //used to trigger final assertions. if its ignored, mocking assertions will never fail defer mockCtrl.Finish() // create a new mock object, passing the controller instance as parameter // for a newly created mock object it will accept any input and outpuite // need to define its behavior with the method expect mockDoer := mocks.NewMockDoer(mockCtrl) testUser := &User{Doer: mockDoer} // // Expect Do to be called once with 123 and "Hello GoMock" as parameters, and return nil from the mocked call. mockDoer.EXPECT().DoSomething(123, "Hello GoMock").Return(nil).Times(1) fmt.Println(testUser.Use()) fmt.Println() } func TestUser_SaySomething(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() mockDoer := mocks.NewMockDoer(mockCtrl) user := User{ Doer: mockDoer, } type args struct { num int } tests := []struct { name string args args want string expect func() wantErr bool }{ { name: "Positive test case 1", expect: func() { mockDoer.EXPECT().SaySomething("spam").Return("bad") }, args: args{num: 3}, wantErr: false, want: "bad", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tt.expect() if got := user.SaySomething(tt.args.num); (got != tt.want) != tt.wantErr { fmt.Println("gott :", got) t.Errorf("User.SaySomething() = %v, want %v", got, tt.want) } }) } }
而单元测试的结果将是这样的:
=== RUN TestUser_SaySomething === RUN TestUser_SaySomething/Positive_test_case_1 --- PASS: TestUser_SaySomething (0.00s) --- PASS: TestUser_SaySomething/Positive_test_case_1 (0.00s) PASS ok github.com/tokopedia/go_learning/udemy/pzn/gomock_test/user 1.100s
经验之谈
可以在不调用外部依赖的情况下进行单元测试。使用mocking
框架可以在很多方面帮助我们,建立干净和轻量级的单元测试。与接口和依赖注入相结合。