本文共 11493 字,大约阅读时间需要 38 分钟。
GoMock是由Golang官方开发维护的测试框架,实现了较为完整的基于interface的Mock功能,能够与Golang内置的testing包良好集成,也能用于其它的测试环境中。GoMock测试框架包含了GoMock包和mockgen工具两部分,其中GoMock包完成对桩对象生命周期的管理,mockgen工具用来生成interface对应的Mock类源文件。
GoMock官网:GoMock安装:go get github.com/golang/mock/gomock
mockgen辅助代码生成工具安装:go get github.com/golang/mock/mockgen
GoMock文档:go doc github.com/golang/mock/gomock
(1)mockgen工具选项
mockgen工具支持的选项如下:-source: 指定接口的源文件-destination: mock类代码的输出文件。如果没有设置本选项,代码将被输出到标准输出。-destination选项输入太长,因此推荐使用重定向符号>将输出到标准输出的内容重定向到某个文件,并且mock类代码的输出文件的路径必须是绝对路径。-package: 指定mock类源文件的包名。如果没有设置本选项,则包名由mock_
和输入文件的包名级联而成。-aux_files: 附加文件列表用于解析嵌套定义在不同文件中的interface。指定元素列表以逗号分隔,元素形式为foo=bar/baz.go,其中bar/baz.go是源文件,foo是-source选项指定的源文件用到的包名。-build_flags: 传递给build工具的参数-imports: 依赖的需要import的包-mock_names:自定义生成mock文件的列表,使用逗号分割。如Repository=MockSensorRepository,Endpoint=MockSensorEndpoint。Repository、Endpoint为接口,MockSensorRepository,MockSensorEndpoint为相应的mock文件。(2)mockgen工作模式mockgen有两种操作模式:源文件模式和反射模式。源文件模式通过一个包含interface定义的源文件生成mock类文件,通过-source标识开启,-imports和-aux_files标识在源文件模式下是有用的。mockgen源文件模式的命令格式如下:mockgen -source=xxxx.go [other options]
反射模式通过构建一个程序用反射理解接口生成一个mock类文件,通过两个非标志参数开启:导入路径和用逗号分隔的符号列表(多个interface)。mockgen反射模式的命令格式如下:mockgen packagepath Interface1,Interface2...
第一个参数是基于GOPATH的相对路径,第二个参数可以为多个interface,并且interface之间只能用逗号分隔,不能有空格。(3)mockgen工作模式适用场景mockgen工作模式适用场景如下:A、对于简单场景,只需使用-source选项。B、对于复杂场景,如一个源文件定义了多个interface而只想对部分interface进行mock,或者interface存在嵌套,则需要使用反射模式。 func InOrder(calls ...*Call)
type Call struct { t TestReporter // for triggering test failures on invalid call setup receiver interface{} // the receiver of the method call method string // the name of the method methodType reflect.Type // the type of the method args []Matcher // the args origin string // file and line number of call setup preReqs []*Call // prerequisite calls // Expectations minCalls, maxCalls int numCalls int // actual number made // actions are called when this Call is called. Each action gets the args and // can set the return values by returning a non-nil slice. Actions run in the // order they are created. actions []func([]interface{}) []interface{}}
Call表示对mock对象的一个期望调用
func (c *Call) After(preReq *Call) *Call
After声明调用在preReq完成后执行func (c *Call) AnyTimes() *Call
允许调用0次或多次func (c *Call) Do(f interface{}) *Call
声明在匹配时要运行的操作func (c *Call) MaxTimes(n int) *Call
设置最大的调用次数为n次func (c *Call) MinTimes(n int) *Call
设置最小的调用次数为n次func (c *Call) Return(rets ...interface{}) *Call
Return声明模拟函数调用返回的值func (c *Call) SetArg(n int, value interface{}) *Call
SetArg声明使用指针设置第n个参数的值func (c *Call) Times(n int) *Call
设置调用的次数为n次func NewController(t TestReporter) *Controller
获取控制对象func WithContext(ctx context.Context, t TestReporter) (*Controller, context.Context)
WithContext返回一个控制器和上下文,如果发生任何致命错误时会取消。func (ctrl *Controller) Call(receiver interface{}, method string, args ...interface{}) []interface{}
Mock对象调用,不应由用户代码调用。func (ctrl *Controller) Finish()
检查所有预计调用的方法是否被调用,每个控制器都应该调用。本函数只应该被调用一次。func (ctrl *Controller) RecordCall(receiver interface{}, method string, args ...interface{}) *Call
被mock对象调用,不应由用户代码调用。func (ctrl *Controller) RecordCallWithMethodType(receiver interface{}, method string, methodType reflect.Type, args ...interface{}) *Call
被mock对象调用,不应由用户代码调用。func Any() Matcher
匹配任意值func AssignableToTypeOf(x interface{}) Matcher
AssignableToTypeOf是一个匹配器,用于匹配赋值给模拟调用函数的参数和函数的参数类型是否匹配。func Eq(x interface{}) Matcher
通过反射匹配到指定的类型值,而不需要手动设置func Nil() Matcher
返回nilfunc Not(x interface{}) Matcher
不递归给定子匹配器的结果 定义一个需要mock的接口Repository,infra/db.go文件如下:
package dbtype Repository interface { Create(key string, value []byte) error Retrieve(key string) ([]byte, error) Update(key string, value []byte) error Delete(key string) error}
mockgen生成mock文件:
mockgen -source=./infra/db.go -destination=./mock/mock_repository.go -package=mock
输出目录./mock必须存在,否则mockgen会运行失败。如果工程中的第三方库统一放在vendor目录下,则需要拷贝一份gomock代码到$GOPATH/src/github.com/golang/mock/gomock
,mockgen命令运行时会在上述路径访问gomock。mock_repository.go文件如下: // Code generated by MockGen. DO NOT EDIT.// Source: ./infra/db.go// Package mock is a generated GoMock package.package mockimport ( gomock "github.com/golang/mock/gomock" reflect "reflect")// MockRepository is a mock of Repository interfacetype MockRepository struct { ctrl *gomock.Controller recorder *MockRepositoryMockRecorder}// MockRepositoryMockRecorder is the mock recorder for MockRepositorytype MockRepositoryMockRecorder struct { mock *MockRepository}// NewMockRepository creates a new mock instancefunc NewMockRepository(ctrl *gomock.Controller) *MockRepository { mock := &MockRepository{ctrl: ctrl} mock.recorder = &MockRepositoryMockRecorder{mock} return mock}// EXPECT returns an object that allows the caller to indicate expected usefunc (m *MockRepository) EXPECT() *MockRepositoryMockRecorder { return m.recorder}// Create mocks base methodfunc (m *MockRepository) Create(key string, value []byte) error { ret := m.ctrl.Call(m, "Create", key, value) ret0, _ := ret[0].(error) return ret0}// Create indicates an expected call of Createfunc (mr *MockRepositoryMockRecorder) Create(key, value interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockRepository)(nil).Create), key, value)}// Retrieve mocks base methodfunc (m *MockRepository) Retrieve(key string) ([]byte, error) { ret := m.ctrl.Call(m, "Retrieve", key) ret0, _ := ret[0].([]byte) ret1, _ := ret[1].(error) return ret0, ret1}// Retrieve indicates an expected call of Retrievefunc (mr *MockRepositoryMockRecorder) Retrieve(key interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Retrieve", reflect.TypeOf((*MockRepository)(nil).Retrieve), key)}// Update mocks base methodfunc (m *MockRepository) Update(key string, value []byte) error { ret := m.ctrl.Call(m, "Update", key, value) ret0, _ := ret[0].(error) return ret0}// Update indicates an expected call of Updatefunc (mr *MockRepositoryMockRecorder) Update(key, value interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockRepository)(nil).Update), key, value)}// Delete mocks base methodfunc (m *MockRepository) Delete(key string) error { ret := m.ctrl.Call(m, "Delete", key) ret0, _ := ret[0].(error) return ret0}// Delete indicates an expected call of Deletefunc (mr *MockRepositoryMockRecorder) Delete(key interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockRepository)(nil).Delete), key)}
package MySQLimport "GoExample/GoMock/infra"type MySQL struct { DB db.Repository}func NewMySQL(db db.Repository) *MySQL { return &MySQL{DB: db}}func (mysql *MySQL) CreateData(key string, value []byte) error { return mysql.DB.Create(key, value)}func (mysql *MySQL) GetData(key string) ([]byte, error) { return mysql.DB.Retrieve(key)}func (mysql *MySQL) DeleteData(key string) error { return mysql.DB.Delete(key)}func (mysql *MySQL) UpdateData(key string, value []byte) error { return mysql.DB.Update(key, value)}
生成mock文件后就可以使用mock对象进行打桩测试,编写测试用例。
(1)导入mock相关包mock相关包包括testing,gomock和mock,import包路径:import ( "testing" "GoExample/GoMock/mock" "github.com/golang/mock/gomock")
(2)mock控制器
mock控制器通过NewController接口生成,是mock生态系统的顶层控制,定义了mock对象的作用域和生命周期,以及mock对象的期望。多个协程同时调用控制器的方法是安全的。当用例结束后,控制器会检查所有剩余期望的调用是否满足条件。ctrl := NewController(t)defer ctrl.Finish()
mock对象创建时需要注入控制器,mock对象注入控制器的代码如下:
ctrl := NewController(t)defer ctrl.Finish()mockRepo := mock_db.NewMockRepository(ctrl)
(3)mock对象的行为注入
对于mock对象的行为注入,控制器通过map来维护,一个方法对应map的一项。因为一个方法在一个用例中可能调用多次,所以map的值类型是数组切片。当mock对象进行行为注入时,控制器会将行为Add。当该方法被调用时,控制器会将该行为Remove。如果先Retrieve领域对象失败,然后Create领域对象成功,再次Retrieve领域对象就能成功。mock对象的行为注入代码如下所示:mockRepo.EXPECT().Retrieve(Any()).Return(nil, ErrAny)mockRepo.EXPECT().Create(Any(), Any()).Return(nil)mockRepo.EXPECT().Retrieve(Any()).Return(objBytes, nil)
当批量Create对象时,可以使用Times关键字:
mockRepo.EXPECT().Create(Any(), Any()).Return(nil).Times(5)
当批量Retrieve对象时,需要注入多次mock行为: mockRepo.EXPECT().Retrieve(Any()).Return(objBytes1, nil)mockRepo.EXPECT().Retrieve(Any()).Return(objBytes2, nil)mockRepo.EXPECT().Retrieve(Any()).Return(objBytes3, nil)mockRepo.EXPECT().Retrieve(Any()).Return(objBytes4, nil)mockRepo.EXPECT().Retrieve(Any()).Return(objBytes5, nil)
(4)行为调用的保序
默认情况下,行为调用顺序可以和mock对象行为注入顺序不一致,即不保序。如果要保序,有两种方法:A、通过After关键字来实现保序B、通过InOrder关键字来实现保序通过After关键字实现的保序示例代码:retrieveCall := mockRepo.EXPECT().Retrieve(Any()).Return(nil, ErrAny)createCall := mockRepo.EXPECT().Create(Any(), Any()).Return(nil).After(retrieveCall)mockRepo.EXPECT().Retrieve(Any()).Return(objBytes, nil).After(createCall)
通过InOrder关键字实现的保序示例代码:
InOrder(mockRepo.EXPECT().Retrieve(Any()).Return(nil, ErrAny)mockRepo.EXPECT().Create(Any(), Any()).Return(nil)mockRepo.EXPECT().Retrieve(Any()).Return(objBytes, nil))
通过InOrder关键字实现保序更简单,关键字InOrder是After的语法糖。
func InOrder(calls ...*Call) { for i := 1; i < len(calls); i++ { calls[i].After(calls[i-1]) }}
当mock对象行为的注入保序后,如果行为调用的顺序和其不一致,就会触发测试失败。如果在测试用例执行过程中,Repository方法的调用顺序如果不是按 Retrieve -> Create -> Retrieve的顺序进行,则会导致测试失败。
(5)mock对象的注入mock对象的行为都注入到控制器后,要将mock对象注入给interface,使得mock对象在测试中生效。通常,当测试用例执行完成后,并没有回滚interface到真实对象,有可能会影响其它测试用例的执行,因此推荐使用GoStub框架完成mock对象的注入。stubs := StubFunc(&mysql,mockdb)defer stubs.Reset()
(6)测试用例编写
MySQL_test.go文件:package MySQLimport ( "testing" "GoExample/GoMock/mock" "fmt" "github.com/golang/mock/gomock")func TestMySQL_CreateData(t *testing.T) { ctr := gomock.NewController(t) defer ctr.Finish() var key string = "Hello" var value []byte = []byte("Go") mockRepository := mock_db.NewMockRepository(ctr) gomock.InOrder( mockRepository.EXPECT().Create(key, value).Return(nil), ) mySQL := NewMySQL(mockRepository) err := mySQL.CreateData(key, value) if err != nil { fmt.Println(err) }}func TestMySQL_GetData(t *testing.T) { ctr := gomock.NewController(t) defer ctr.Finish() var key string = "Hello" var value []byte = []byte("Go") mockRepository := mock_db.NewMockRepository(ctr) gomock.InOrder( mockRepository.EXPECT().Retrieve(key).Return(value, nil), ) mySQL := NewMySQL(mockRepository) bytes, err := mySQL.GetData(key) if err != nil { fmt.Println(err) } else { fmt.Println(string(bytes)) }}func TestMySQL_UpdateData(t *testing.T) { ctr := gomock.NewController(t) defer ctr.Finish() var key string = "Hello" var value []byte = []byte("Go") mockRepository := mock_db.NewMockRepository(ctr) gomock.InOrder( mockRepository.EXPECT().Update(key, value).Return(nil), ) mySQL := NewMySQL(mockRepository) err := mySQL.UpdateData(key, value) if err != nil { fmt.Println(err) }}func TestMySQL_DeleteData(t *testing.T) { ctr := gomock.NewController(t) defer ctr.Finish() var key string = "Hello" mockRepository := mock_db.NewMockRepository(ctr) gomock.InOrder( mockRepository.EXPECT().Delete(key).Return(nil), ) mySQL := NewMySQL(mockRepository) err := mySQL.DeleteData(key) if err != nil { fmt.Println(err) }}
进入测试用例目录:
go test .
生成测试覆盖率的 profile 文件:
go test -coverprofile=cover.out .
利用 profile 文件生成可视化界面go tool cover -html=cover.out
转载于:https://blog.51cto.com/9291927/2346777