GoMock是由Golang官方開發維護的測試框架,實現了較爲完整的基於interface的Mock功能,可以與Golang內置的testing包良好集成,也能用於其它的測試環境中。GoMock測試框架包含了GoMock包和mockgen工具兩部分,其中GoMock包完成對樁對象生命週期的管理,mockgen工具用來生成interface對應的Mock類源文件。
GoMock官網:
https://github.com/golang/mock
GoMock安裝:go get github.com/golang/mock/gomock
mockgen輔助代碼生成工具安裝:go get github.com/golang/mock/mockgen
GoMock文檔:go doc github.com/golang/mock/gomock
html
(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存在嵌套,則須要使用反射模式。mysql
func InOrder(calls ...*Call)
InOrder聲明給定調用的調用順序git
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
不遞歸給定子匹配器的結果github
定義一個須要mock的接口Repository,infra/db.go文件以下:golang
package db type 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文件以下:sql
// Code generated by MockGen. DO NOT EDIT. // Source: ./infra/db.go // Package mock is a generated GoMock package. package mock import ( gomock "github.com/golang/mock/gomock" reflect "reflect" ) // MockRepository is a mock of Repository interface type MockRepository struct { ctrl *gomock.Controller recorder *MockRepositoryMockRecorder } // MockRepositoryMockRecorder is the mock recorder for MockRepository type MockRepositoryMockRecorder struct { mock *MockRepository } // NewMockRepository creates a new mock instance func 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 use func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder { return m.recorder } // Create mocks base method func (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 Create func (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 method func (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 Retrieve func (mr *MockRepositoryMockRecorder) Retrieve(key interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Retrieve", reflect.TypeOf((*MockRepository)(nil).Retrieve), key) } // Update mocks base method func (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 Update func (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 method func (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 Delete func (mr *MockRepositoryMockRecorder) Delete(key interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockRepository)(nil).Delete), key) }
package MySQL import "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對象的行爲注入代碼以下所示:ide
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 MySQL import ( "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