go test 測試用例那些事(二) mock
- 2020 年 7 月 21 日
- 筆記
- Go, go mock, go mockgen, golang, golang mock, gomock, mock, mockgen
關於go
的單元測試,之前有寫過一篇帖子go test測試用例那些事,但是沒有說go官方的庫mock,很有必要單獨說一下這個庫,和他的實現原理。
mock
主要的功能是對介面的模擬,需要在寫程式碼的時候定義抽象很多介面,有時為了能方便go test
可能會多寫一些冗餘程式碼,但這些工作會讓你的單元測試更靈活。特別是邏輯比較複雜的時候,上層要調用其他層的方法進行單元測試,會讓單元測試越寫越麻煩,越寫越複雜,這也是很多人不喜歡寫單元測試的原因。使用mock
模擬底層的介面,能讓你只關註上層需要測試的邏輯,而不用為了測試一個功能,寫一堆調用的底層的相關的測試邏輯。
使用
mockgen
就是mock的可執行命令。使用也很簡單
mockgen -source=src.go [other options]
比如我們有一個介面
package d1
type User interface {
Name() string
SetAge(age int) bool
V(idx int, name string) (string, error)
}
執行mockgen
命令
mockgen -source=user.go
這裡只指寫了-source
會直接在控制台輸出。也可以指定輸出目錄和輸出包名稱
mockgen -source=user.go -destination ./dao/u_mock.go -package mock_data
或者使用 go generate
來生成,需要在包名字上面加上下面這句。
//go:generate mockgen -destination ./dao/u_mock.go -package mock_data -source user.go
然後執行go generate ./...
和上面是一樣的效果。
雖然go generate
很方便,但如果目標文件或者包名字有變動里,就需要修改所有文件。不如用命令來的快,直接寫一個Makefile
進行指處理,下面是一個小例子,實現mock
目錄dao
和service
下的go
文件,去掉了*_test.go
和一些指定的文件。
DAO_DIR=./dao
DAO_MOCK_DIR=$(DAO_DIR)/mock_dao
DAO_FILES=$(shell find $(DAO_DIR) -not -path "$(DAO_MOCK_DIR)/*" -type f -name "*.go" -not -name "*_test.go" -not -name "dao_init.go" -not -name "dao.go")
SERVICE_DIR=./service
SERVICE_MOCK_DIR=$(SERVICE_DIR)/mock_srv
SERVICE_FILES=$(shell find $(SERVICE_DIR) -not -path "$(SERVICE_MOCK_DIR)/*" -type f -name "*.go" -not -name "*_test.go" -not -name "service.go" -not -name "system_filter.go")
define gen-mock-file
@for f in $(3); do \
eval t=`echo $$f | sed 's#$(1)#$(2)#'` ; \
mockgen -source=$$f -destination=$$t ; \
done
endef
.PHONY: gen-mock-dao
gen-mock-dao:
$(call gen-mock-file,$(DAO_DIR),$(DAO_MOCK_DIR),$(DAO_FILES))
.PHONY: gen-mock-service
gen-mock-service:
$(call gen-mock-file,$(SERVICE_DIR),$(SERVICE_MOCK_DIR),$(SERVICE_FILES))
gen-mock-all:
@echo begin gen code
@$(MAKE) gen-mock-dao
@$(MAKE) gen-mock-service
@echo done
使用
使用也很簡單直接調用EXPECT()
然後給具體的方法指定參數,參數可以是任意的如下面的V
方法的第一個參數gomock.Any()
,參數可以是具體的值比如下面的2
,然後調用Return
指寫返回指定的值。最後指定這個方法調用多少次,下面是調用的AnyTimes()
,當然也可以調用MinTimes
或者MaxTimes
指定次數
func TestUser1(t *testing.T) {
mockUser := mock_data.NewMockUser(gomock.NewController(t))
mockUser.EXPECT().V(gomock.Any(), "2").Return("a", nil).AnyTimes()
var u User = mockUser
a, err := u.V(1, "2")
t.Log(a, err)
}
Return
如果不調用會返回參數的默認值,上面的方法不如果不調用Return
會返回 "", nil
。
對於簡單的邏輯可以直接調用Return
方法,返回指定的結果。但實際情況可能需要進行一些邏輯處理,返回動態的數據,可能通過DoAndReturn
mockUser := mock_data.NewMockUser(gomock.NewController(t))
mockUser.EXPECT().V(1, "2").DoAndReturn(func(idx int, n string) (string, error) {
t.Log(idx, " ", n)
return "1", nil
})
可以有多個DoAndReturn
,但只有最後一個的 return
會生效。
如果只想對傳入的參數進行邏輯處理,可以調用Do
方法。
mockUser.EXPECT().V(1, "2").Do(func(id int, name string) {
t.Log(id, " ", name)
}).Do(func(id int, name string) {
t.Log("do2 ", id)
}).Return("a", nil)
當然根據自己的需要可以有多個Do
方法的處理。
mock
實現原理
實現的原理是根據go
強大的抽象語法樹
實現的,說一個題外話除了mock庫,還有一個依賴注入的庫wire也是依賴抽象語法樹實現的。
抽象語法樹分析-source
傳入的文件,把提取文件內所有的import
和interface
,然後遍歷所有的介面方法,判斷參數屬於哪個import
,組織成結構,生成模擬結構實現提取的介面。
看一下生成的兩個struct
// MockUser is a mock of User interface
type MockUser struct {
ctrl *gomock.Controller
recorder *MockUserMockRecorder
}
// MockUserMockRecorder is the mock recorder for MockUser
type MockUserMockRecorder struct {
mock *MockUser
}
上面的MockUser
具體實現了我們的介面User
。下面的MockUserMockRecorder
才是重頭戲,保存著我們傳入的的指定參數傳Do
方法Return
方法等。
// NewMockUser creates a new mock instance
func NewMockUser(ctrl *gomock.Controller) *MockUser {
mock := &MockUser{ctrl: ctrl}
mock.recorder = &MockUserMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockUser) EXPECT() *MockUserMockRecorder {
return m.recorder
}
EXPECT()
方法返回的就是MockUserMockRecorder
看一下我們的例子方法V
// V mocks base method
func (m *MockUser) V(idx int, name string) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "V", idx, name)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// V indicates an expected call of V
func (mr *MockUserMockRecorder) V(idx, name interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "V", reflect.TypeOf((*MockUser)(nil).V), idx, name)
}
返回的*gomock.Call
就是最底層的數據結構,保存的所有的自定義參數
type Call struct {
t TestHelper // 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{}
}
method``methodType
保存的方法的資訊,mock
是從反射欄位methodType
知道傳入參數和返回結果的資訊。args
用於保存指定的參數, 是gomock.Any()
還是gomock.Eq()
等,進行傳入參數匹配。minCalls maxCalls
用於保存調用次數的限制actions
用於保存我們的方法自定義方法Do
Return
DoReturn
等。