Go 單元測試--Mock介面實現和對介面打樁

語言: CN / TW / HK

這是Go語言單元測試系列教程的第4篇,介紹瞭如何在單元測試中使用gomock和gostub工具mock介面和打樁。

在上一篇 《Go單元測試 — 資料庫 CRUD 的 Mock 測試》 中,我們介紹瞭如何使用 go-sqlmockminiredis 工具進行資料庫測試。除了網路和資料庫等外部依賴之外,我們在開發中也會經常用到各種各樣的介面型別。本文就舉例來演示如何在編寫單元測試的時候對介面型別進行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/[email protected]

Go版本>=1.16時:

go install github.com/golang/mock/[email protected]

如果是在你的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
    
  • -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/mockmockery

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

系列文章推薦:

  1. Go單元測試從入門到放棄—0.單元測試基礎

  2. Go單元測試--模擬服務請求和介面返回

  3. Go單測測試 — 資料庫 CRUD 的 Mock 測試

- END -

掃碼關注公眾號「網管叨bi叨」

給網管個星標,第一時間吸我的知識 :point_up_2:

網管為大家整理了一本超實用的《Go 開發參考書》收集了70多條開發實踐。去公眾號回覆【gocookbook】即刻領取!

覺得有用就點個在看   :point_down::point_down::point_down: