Go Mock -gomock- 簡明教程
1 gomock 簡介
上一篇文章 Go Test 單元測試簡明教程 介紹了 Go 語言中單元測試的常用方法,包括子測試 (subtests)、表格驅動測試(table-driven tests)、幫助函數(helpers)、網絡測試和基準測試(Benchmark) 等。這篇文章介紹一種新的測試方法,mock/stub 測試,當待測試的函數 / 對象的依賴關係很複雜,並且有些依賴不能直接創建,例如數據庫連接、文件 I/O 等。這種場景就非常適合使用 mock/stub 測試。簡單來說,就是用 mock 對象模擬依賴項的行爲。
GoMock is a mocking framework for the Go programming language. It integrates well with Go’s built-in testing package, but can be used in other contexts too.
gomock 是官方提供的 mock 框架,同時還提供了 mockgen 工具用來輔助生成測試代碼。
使用如下命令即可安裝:
go get -u github.com/golang/mock/gomock
go get -u github.com/golang/mock/mockgen
2 一個簡單的 Demo
type DB interface {
Get(key string) (int, error)
}
func GetFromDB(db DB, key string) int {
if value, err := db.Get(key); err == nil {
return value
}
return -1
}
假設 DB
是代碼中負責與數據庫交互的部分 (在這裏用 map 模擬),測試用例中不能創建真實的數據庫連接。這個時候,如果我們需要測試 GetFromDB
這個函數內部的邏輯,就需要 mock 接口 DB
。
第一步:使用 mockgen
生成 db_mock.go
。一般傳遞三個參數。包含需要被 mock 的接口得到源文件source
,生成的目標文件destination
,包名package
。
$ mockgen -source=db.go -destination=db_mock.go -package=main
第二步:新建 db_test.go
,寫測試用例。
func TestGetFromDB(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
m := NewMockDB(ctrl)
m.EXPECT().Get(gomock.Eq("Tom")).Return(100, errors.New("not exist"))
if v := GetFromDB(m, "Tom"); v != -1 {
t.Fatal("expected -1, but got", v)
}
}
- 這個測試用例有 2 個目的,一是使用
ctrl.Finish()
斷言DB.Get()
被是否被調用,如果沒有被調用,後續的 mock 就失去了意義; - 二是測試方法
GetFromDB()
的邏輯是否正確 (如果DB.Get()
返回 error,那麼GetFromDB()
返回 -1)。 NewMockDB()
的定義在db_mock.go
中,由 mockgen 自動生成。
最終的代碼結構如下:
project/
|--db.go
|--db_mock.go // generated by mockgen
|--db_test.go
執行測試:
$ go test . -cover -v
=== RUN TestGetFromDB
--- PASS: TestGetFromDB (0.00s)
PASS
coverage: 81.2% of statements
ok example 0.008s coverage: 81.2% of statements
3 打樁 (stubs)
在上面的例子中,當 Get()
的參數爲 Tom,則返回 error,這稱之爲打樁(stub)
,有明確的參數和返回值是最簡單打樁方式。除此之外,檢測調用次數、調用順序,動態設置返回值等方式也經常使用。
3.1 參數 (Eq, Any, Not, Nil)
m.EXPECT().Get(gomock.Eq("Tom")).Return(0, errors.New("not exist"))
m.EXPECT().Get(gomock.Any()).Return(630, nil)
m.EXPECT().Get(gomock.Not("Sam")).Return(0, nil)
m.EXPECT().Get(gomock.Nil()).Return(0, errors.New("nil"))
Eq(value)
表示與 value 等價的值。Any()
可以用來表示任意的入參。Not(value)
用來表示非 value 以外的值。Nil()
表示 None 值
3.2 返回值 (Return, DoAndReturn)
m.EXPECT().Get(gomock.Not("Sam")).Return(0, nil)
m.EXPECT().Get(gomock.Any()).Do(func(key string) {
t.Log(key)
})
m.EXPECT().Get(gomock.Any()).DoAndReturn(func(key string) (int, error) {
if key == "Sam" {
return 630, nil
}
return 0, errors.New("not exist")
})
Return
返回確定的值Do
Mock 方法被調用時,要執行的操作嗎,忽略返回值。DoAndReturn
可以動態地控制返回值。
3.3 調用次數 (Times)
func TestGetFromDB(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
m := NewMockDB(ctrl)
m.EXPECT().Get(gomock.Not("Sam")).Return(0, nil).Times(2)
GetFromDB(m, "ABC")
GetFromDB(m, "DEF")
}
Times()
斷言 Mock 方法被調用的次數。MaxTimes()
最大次數。MinTimes()
最小次數。AnyTimes()
任意次數(包括 0 次)。
3.4 調用順序 (InOrder)
func TestGetFromDB(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
m := NewMockDB(ctrl)
o1 := m.EXPECT().Get(gomock.Eq("Tom")).Return(0, errors.New("not exist"))
o2 := m.EXPECT().Get(gomock.Eq("Sam")).Return(630, nil)
gomock.InOrder(o1, o2)
GetFromDB(m, "Tom")
GetFromDB(m, "Sam")
}
4 如何編寫可 mock 的代碼
寫可測試的代碼與寫好測試用例是同等重要的,如何寫可 mock 的代碼呢?
- mock 作用的是接口,因此將依賴抽象爲接口,而不是直接依賴具體的類。
- 不直接依賴的實例,而是使用依賴注入降低耦合性。
在軟件工程中,依賴注入的意思爲,給予調用方它所需要的事物。 “依賴”是指可被方法調用的事物。依賴注入形式下,調用方不再直接指使用 “依賴”,取而代之是“注入” 。“注入” 是指將 “依賴” 傳遞給調用方的過程。在 “注入” 之後,調用方纔會調用該“依賴”。傳遞依賴給調用方,而不是讓讓調用方直接獲得依賴,這個是該設計的根本需求。
– 依賴注入 - Wikipedia
如果 GetFromDB()
方法長這個樣子
func GetFromDB(key string) int {
db := NewDB()
if value, err := db.Get(key); err == nil {
return value
}
return -1
}
對 DB
接口的 mock 並不能作用於 GetFromDB()
內部,這樣寫是沒辦法進行測試的。那如果將接口 db DB
通過參數傳遞到 GetFromDB()
,那麼就可以輕而易舉地傳入 Mock 對象了。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://geektutu.com/post/quick-gomock.html