Go 單測從零到溜系列—3- 接口測試
這是 Go 語言單元測試從零到溜系列教程的第 3 篇,介紹瞭如何在單元測試中使用 gomock 和 gostub 工具 mock 接口和打樁。
在上一篇《Go 單測從零到溜系列 2—數據庫測試》中,我們介紹瞭如何使用go-sqlmock
和miniredis
工具進行數據庫測試。
除了網絡和數據庫等外部依賴之外,我們在開發中也會經常用到各種各樣的接口類型。本文就舉例來演示如何在編寫單元測試的時候對接口類型進行 mock 以及如何進行打樁。
gomock
gomock 是 Go 官方提供的測試框架,它在內置的 testing 包或其他環境中都能夠很方便的使用。我們使用它對代碼中的那些接口類型進行 mock,方便編寫單元測試。
安裝 mockgen
互聯網開源庫更新迭代比較快,建議直接查看官方文檔:https://github.com/golang/mock
首先需要確保你的$GOPATH/bin
已經加入到環境變量中。
Go 版本號 < 1.16 時:
GO111MODULE=on go get github.com/golang/mock/mockgen@v1.6.0
Go 版本 >=1.16 時:
go install github.com/golang/mock/mockgen@v1.6.0
如果是在你的 CI 流水線中安裝,則需要安裝與你的 CI 環境匹配的合適版本。
運行 mockgen
mockgen
有兩種操作模式:源碼(source)模式和反射(reflect)模式。
源碼模式
源碼模式根據源文件 mock 接口。它是通過使用 -source
標誌啓用。在這個模式下可能有用的其他標誌是 -imports
和 -aux_files
。
例如:
mockgen -source=foo.go [other options]
反射模式
反射模式通過構建使用反射來理解接口的程序來 mock 接口。它是通過傳遞兩個非標誌參數來啓用的:一個導入路徑和一個逗號分隔的符號列表。可以使用 ”.” 引用當前路徑的包。
例如:
mockgen database/sql/driver Conn,Driver
# Convenient for `go:generate`.
mockgen . Conn,Driver
flags
mockgen
命令用來爲給定一個包含要 mock 的接口的 Go 源文件,生成 mock 類源代碼。它支持以下標誌:
-
-source
:包含要 mock 的接口的文件。 -
-destination
:生成的源代碼寫入的文件。如果不設置此項,代碼將打印到標準輸出。 -
-package
:用於生成的模擬類源代碼的包名。如果不設置此項包名默認在原包名前添加mock_
前綴。 -
-imports
:在生成的源代碼中使用的顯式導入列表。值爲 foo=bar/baz 形式的逗號分隔的元素列表,其中 bar/baz 是要導入的包,foo 是要在生成的源代碼中用於包的標識符。 -
-aux_files
:需要參考以解決的附加文件列表,例如在不同文件中定義的嵌入式接口。指定的值應爲 foo=bar/baz.go 形式的以逗號分隔的元素列表,其中 bar/baz.go 是源文件,foo 是-source
文件使用的文件的包名。 -
-build_flags
:(僅反射模式)一字不差地傳遞標誌給 go build -
-mock_names
:生成的模擬的自定義名稱列表。這被指定爲一個逗號分隔的元素列表,形式爲Repository = MockSensorRepository,Endpoint=MockSensorEndpoint
,其中Repository
是接口名稱,mockSensorrepository
是所需的 mock 名稱 (mock 工廠方法和 mock 記錄器將以 mock 命名)。如果其中一個接口沒有指定自定義名稱,則將使用默認命名約定。 -
-self_package
:生成的代碼的完整包導入路徑。使用此 flag 的目的是通過嘗試包含自己的包來防止生成代碼中的循環導入。如果 mock 的包被設置爲它的一個輸入 (通常是主輸入),並且輸出是 stdio,那麼 mockgen 就無法檢測到最終的輸出包,這種情況就會發生。設置此標誌將告訴 mockgen 排除哪個導入的包 -
-copyright_file
:用於將版權標頭添加到生成的源代碼中的版權文件 -
-debug_parser
:僅打印解析器結果 -
-exec_only
:(反射模式) 如果設置,則執行此反射程序 -
-prog_only
:(反射模式)只生成反射程序;將其寫入標準輸出並退出。 -
-write_package_comment
:如果爲 true,則寫入包文檔註釋 (godoc)。(默認爲 true)
構建 mock
這裏就以日常開發中經常用到的數據庫操作爲例,講解一下如何使用 gomock 來 mock 接口的單元測試。
假設有查詢 MySQL 數據庫的業務代碼如下,其中DB
是一個自定義的接口類型:
// db.go
// DB 數據接口
type DB interface {
Get(key string)(int, error)
Add(key string, value int) error
}
// GetFromDB 根據key從DB查詢數據的函數
func GetFromDB(db DB, key string) int {
if v, err := db.Get(key);err == nil{
return v
}
return -1
}
我們現在要爲GetFromDB
函數編寫單元測試代碼,可是我們又不能在單元測試過程中連接真實的數據庫,這個時候就需要 mock DB
這個接口來方便進行單元測試。
使用上面提到的 mockgen
工具來爲生成相應的 mock 代碼。通過執行下面的命令,我們就能在當前項目下生成一個mocks
文件夾,裏面存放了一個db_mock.go
文件。
mockgen -source=db.go -destination=mocks/db_mock.go -package=mocks
db_mock.go
文件中的內容就是 mock 相關接口的代碼了。
我們通常不需要編輯它,只需要在單元測試中按照規定的方式使用它們就可以了。例如,我們編寫TestGetFromDB
函數如下:
// db_test.go
func TestGetFromDB(t *testing.T) {
// 創建gomock控制器,用來記錄後續的操作信息
ctrl := gomock.NewController(t)
// 斷言期望的方法都被執行
// Go1.14+的單測中不再需要手動調用該方法
defer ctrl.Finish()
// 調用mockgen生成代碼中的NewMockDB方法
// 這裏mocks是我們生成代碼時指定的package名稱
m := mocks.NewMockDB(ctrl)
// 打樁(stub)
// 當傳入Get函數的參數爲liwenzhou.com時返回1和nil
m.
EXPECT().
Get(gomock.Eq("liwenzhou.com")). // 參數
Return(1, nil). // 返回值
Times(1) // 調用次數
// 調用GetFromDB函數時傳入上面的mock對象m
if v := GetFromDB(m, "liwenzhou.com"); v != 1 {
t.Fatal()
}
}
打樁(stub)
軟件測試中的打樁是指用一些代碼(樁 stub)代替目標代碼,通常用來屏蔽或補齊業務邏輯中的關鍵代碼方便進行單元測試。
屏蔽:不想在單元測試用引入數據庫連接等重資源
補齊:依賴的上下游函數或方法還未實現
上面代碼中就用到了打樁,當傳入Get
函數的參數爲liwenzhou.com
時就返回1, nil
的返回值。
gomock
支持針對參數、返回值、調用次數、調用順序等進行打樁操作。
參數
參數相關的用法有:
-
gomock.Eq(value):表示一個等價於 value 值的參數
-
gomock.Not(value):表示一個非 value 值的參數
-
gomock.Any():表示任意值的參數
-
gomock.Nil():表示空值的參數
-
SetArg(n, value):設置第 n(從 0 開始)個參數的值,通常用於指針參數或切片
具體示例如下:
m.EXPECT().Get(gomock.Not("q1mi")).Return(10, nil)
m.EXPECT().Get(gomock.Any()).Return(20, nil)
m.EXPECT().Get(gomock.Nil()).Return(-1, nil)
這裏單獨說一下SetArg
的適用場景,假設你有一個需要 mock 的接口如下:
type YourInterface {
SetValue(arg *int)
}
此時,打樁的時候就可以使用SetArg
來修改參數的值。
m.EXPECT().SetValue(gomock.Any()).SetArg(0, 7) // 將SetValue的第一個參數設置爲7
返回值
gomock 中跟返回值相關的用法有以下幾個:
-
Return():返回指定值
-
Do(func):執行操作,忽略返回值
-
DoAndReturn(func):執行並返回指定值
例如:
m.EXPECT().Get(gomock.Any()).Return(20, nil)
m.EXPECT().Get(gomock.Any()).Do(func(key string) {
t.Logf("input key is %v\n", key)
})
m.EXPECT().Get(gomock.Any()).DoAndReturn(func(key string)(int, error) {
t.Logf("input key is %v\n", key)
return 10, nil
})
調用次數
使用 gomock 工具 mock 的方法都會有期望被調用的次數,默認每個 mock 方法只允許被調用一次。
m.
EXPECT().
Get(gomock.Eq("liwenzhou.com")). // 參數
Return(1, nil). // 返回值
Times(1) // 設置Get方法期望被調用次數爲1
// 調用GetFromDB函數時傳入上面的mock對象m
if v := GetFromDB(m, "liwenzhou.com"); v != 1 {
t.Fatal()
}
// 再次調用上方mock的Get方法時不滿足調用次數爲1的期望
if v := GetFromDB(m, "liwenzhou.com"); v != 1 {
t.Fatal()
}
gomock 爲我們提供瞭如下方法設置期望被調用的次數。
-
Times()
斷言 Mock 方法被調用的次數。 -
MaxTimes()
最大次數。 -
MinTimes()
最小次數。 -
AnyTimes()
任意次數(包括 0 次)。
調用順序
gomock 還支持使用InOrder
方法指定 mock 方法的調用順序:
// 指定順序
gomock.InOrder(
m.EXPECT().Get("1"),
m.EXPECT().Get("2"),
m.EXPECT().Get("3"),
)
// 按順序調用
GetFromDB(m, "1")
GetFromDB(m, "2")
GetFromDB(m, "3")
此外知名的 Go 測試庫 testify 目前也提供類似的 mock 工具—testify/mock
和mockery
。
GoStub
GoStub 也是一個單元測試中的打樁工具,它支持爲全局變量、函數等打樁。
不過我個人感覺它爲函數打樁不太方便,我一般在單元測試中只會使用它來爲全局變量打樁。
安裝
go get github.com/prashantv/gostub
使用示例
這裏使用官方文檔中的示例代碼演示如何使用 gostub 爲全局變量打樁。
// app.go
var (
configFile = "config.json"
maxNum = 10
)
func GetConfig() ([]byte, error) {
return ioutil.ReadFile(configFile)
}
func ShowNumber()int{
// ...
return maxNum
}
上面代碼中定義了兩個全局變量和兩個使用全局變量的函數,我們現在爲這兩個函數編寫單元測試。
// app_test.go
import (
"github.com/prashantv/gostub"
"testing"
)
func TestGetConfig(t *testing.T) {
// 爲全局變量configFile打樁,給它賦值一個指定文件
stubs := gostub.Stub(&configFile, "./test.toml")
defer stubs.Reset() // 測試結束後重置
// 下面是測試的代碼
data, err := GetConfig()
if err != nil {
t.Fatal()
}
// 返回的data的內容就是上面/tmp/test.config文件的內容
t.Logf("data:%s\n", data)
}
func TestShowNumber(t *testing.T) {
stubs := gostub.Stub(&maxNum, 20)
defer stubs.Reset()
// 下面是一些測試的代碼
res := ShowNumber()
if res != 20 {
t.Fatal()
}
}
執行單元測試,查看結果:
❯ go test -v
=== RUN TestGetConfig
app_test.go:18: data:blog="liwenzhou.com"
--- PASS: TestGetConfig (0.00s)
=== RUN TestShowNumber
--- PASS: TestShowNumber (0.00s)
PASS
ok golang-unit-test-demo/gostub_demo 0.012s
從上面的示例中我們可以看到,在單元測試中使用gostub
可以很方便的對全局變量進行打樁,將其 mock 成我們預期的值從而進行測試。
總結
在日常工作開發中爲代碼編寫單元測試時如何處理代碼中的接口類型是十分常見的問題,本文介紹瞭如何使用gomock
mock 相關接口和如何使用gostub
工具對全局變量進行打樁。
在下一篇中,我們將更進一步,詳細介紹如何在編寫單元測試時使用更全能的打樁工具——monkey
。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/LDHNMil4_QZFUkdCXDIoRQ