在實際開發專案進行單元測試的時候, 卻往往發現有一大堆的依賴, 這時就是 gomock
大顯身手的時候了
Gomock
為 Go 的一個 mock framework, 由官方提供並維護
$ go get -u github.com/golang/mock/gomock
$ go install github.com/golang/mock/mockgen
首先需要安裝 gomock
和 mock 程式碼的生成工具 mockgen
, 後者可以大幅節省工作量, 只需要瞭解其使用方式即可
在 mockgen
指令中支援兩種生成模式:
-
source: 從 source code 生成 mock interface (通過
-source
啟用)mockgen -source=foo.go [other options]
-
reflect: 通過使用反射來生成 mock interface, 其通過傳遞兩個非標誌參數啟用: 導入路徑和以逗號區隔的 interface 列表
mockgen database/sql/driver Conn,Driver
本質上來說上述兩種模式生成的 mock 程式碼並無區別, 選擇合適的使用即可
下面會示範模擬一個簡單的 test case 以熟悉整體的測試流程
測試步驟如下:
- 構思整體測試邏輯
- 定義想要模擬的依賴項的 interface
- 使用
mockgen
指令對所需 mock 的 interface 生成 mock 文件 - 編寫單元測試邏輯, 在測試中使用 mock
- 進行單元測試驗證
在 person/male.go
檔案定義 Male
interface:
package person
type Male interface {
Get(id int64) error
}
在 user/user.go
檔案中調用 Male
的 Get()
方法:
package user
type User struct {
Person person.Male
}
func NewUser(p person.Male) *User {
return &User{Person: p}
}
func (u *User) GetUserInfo(id int64) error {
return u.Person.Get(id)
}
回到根目錄執行以下指令:
$ mockgen -source=./person/male.go -destination=./mock/male_mock.go -package=mock
- source: 設置需要 mock 的 interface 檔案
- destination: 設置 mock 檔案輸出的位置, 默認輸出 Stdout
- package: 設置 mock 文件的 package name, 默認為
mock_filename(mock_person)
- 完整參數參考官方手冊
執行完畢後發現 mock/
目錄下多了一個 male_mock.go
的檔案, 即為 mock 檔案
輸出後的 mock file 如下:
// Code generated by MockGen. DO NOT EDIT.
// Source: ./person/male.go
// Package mock is a generated GoMock package.
package mock
import (
gomock "github.com/golang/mock/gomock"
reflect "reflect"
)
// MockMale is a mock of Male interface
type MockMale struct {
ctrl *gomock.Controller
recorder *MockMaleMockRecorder
}
// MockMaleMockRecorder is the mock recorder for MockMale
type MockMaleMockRecorder struct {
mock *MockMale
}
// NewMockMale creates a new mock instance
func NewMockMale(ctrl *gomock.Controller) *MockMale {
mock := &MockMale{ctrl: ctrl}
mock.recorder = &MockMaleMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockMale) EXPECT() *MockMaleMockRecorder {
return m.recorder
}
// Get mocks base method
func (m *MockMale) Get(id int64) error {
ret := m.ctrl.Call(m, "Get", id)
ret0, _ := ret[0].(error)
return ret0
}
// Get indicates an expected call of Get
func (mr *MockMaleMockRecorder) Get(id interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockMale)(nil).Get), id)
}
在 user/user_test.go
檔案編寫 test cases:
package user
func TestUser_GetUserInfo(t *testing.T) {
ctl := gomock.NewController(t)
defer ctl.Finish()
var id int64 = 1
mockMale := mock.NewMockMale(ctl)
gomock.InOrder(
mockMale.EXPECT().Get(id).Return(nil),
)
user := NewUser(mockMale)
err := user.GetUserInfo(id)
if err != nil {
t.Errorf("user.GetUserInfo err: %v", err)
}
}
- gomock.NewController: 返回
gomock.Controller
, 其代表 mock 生態系統中的頂層控件, 定義了 mock 物件的範圍, 生命週期和期望值, 另其為 thread-safe - mock.NewMockMale: 創建一個新的 mock instance
- gomock.InOrder: 聲明給定的調用應按順序進行(為對
gomock.After
的二次封裝) - mockMale.EXPECT().Get(id).Return(nil):
- EXPECT(): 返回一個允許調用者設置
期望值
和返回值
的物件 - Get(id): 設置
input parameter
並調用 mock instance 的方法 - Return(nil): 設置先前調用方法的
output parameter
- EXPECT(): 返回一個允許調用者設置
- NewUser(mockMale): 創建 User instance, 這裡注入 mock 物件, 因此實際上在後面的
user.GetUserInfo(id)
調用中調用的是事先模擬好的 mock 方法(input parameter id 為 1)
- ctl.Finish(): 進行 mock instance 的期望值斷言, 一般會使用
defer
延遲執行以確保執行
回到根目錄並執行以下指令:
$ go test ./user
ok github.com/regy/mockd/user
即完成單元測試, 可以透過調整 Return()
返回值來得到不一樣的測試結果
$ go test -cover ./user
ok github.com/regy/mockd/user (cached) coverage: 100.0% of statements
可以通過設置 -cover
參數來開啟 coverage ratio 的統計
-
生成 test coverage 的 profile 檔案:
$ go test ./... -coverprofile=cover.out
-
利用 profile 檔案生成可視化介面:
$ go tool cover -html=cover.out
即可透過可視化介面查看 test coverage 情況
調用方法:
- Call.Do(): 聲明匹配時要運行的操作
- Call.DoAndReturn(): 聲明匹配時要運行的操作, 且模擬返回該函式的返回值
- Call.MaxTimes(): 設置最大的調用次數為 n 次
- Call.MinTimes(): 設置最小的調用次數為 n 次
- Call.AnyTimes(): 允許調用次數為 0 或更多次
- Call.Times(): 設置調用次數為 n 次
參數匹配:
- gomock.Any(): 匹配任意值
- gomock.Eq(): 通過反射匹配到指定型別值, 無需手動設置
- gomock.Nil(): 返回
nil
可以利用 go:generate
來完成批量處理功能:
go generate [-run regexp] [-n] [-v] [-x] [build flags] [file.go... | packages]