1. 程式人生 > >go test 測試用例那些事(二) mock

go test 測試用例那些事(二) mock

關於`go`的單元測試,之前有寫過一篇帖子[go test測試用例那些事](https://www.cnblogs.com/li-peng/p/10036468.html),但是沒有說go官方的庫[mock](https://github.com/golang/mock),很有必要單獨說一下這個庫,和他的實現原理。 `mock`主要的功能是對介面的模擬,需要在寫程式碼的時候定義抽象很多介面,有時為了能方便`go test`可能會多寫一些冗餘程式碼,但這些工作會讓你的單元測試更靈活。特別是邏輯比較複雜的時候,上層要呼叫其他層的方法進行單元測試,會讓單元測試越寫越麻煩,越寫越複雜,這也是很多人不喜歡寫單元測試的原因。使用`mock`模擬底層的介面,能讓你只關注上層需要測試的邏輯,而不用為了測試一個功能,寫一堆呼叫的底層的相關的測試邏輯。 ## 使用 `mockgen`就是[mock](https://github.com/golang/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 ./...`和上面是一樣的效果。 ![](https://img2020.cnblogs.com/blog/342595/202007/342595-20200720151944414-1537264769.png) 雖然`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](https://github.com/golang/mock)庫,還有一個依賴注入的庫[wire](https://github.com/google/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` `DoRetur