Go 單測從零到溜系列—3- 接口測試

這是 Go 語言單元測試從零到溜系列教程的第 3 篇,介紹瞭如何在單元測試中使用 gomock 和 gostub 工具 mock 接口和打樁。

在上一篇《Go 單測從零到溜系列 2—數據庫測試》中,我們介紹瞭如何使用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/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 類源代碼。它支持以下標誌:

構建 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支持針對參數、返回值、調用次數、調用順序等進行打樁操作。

參數

參數相關的用法有:

具體示例如下:

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 中跟返回值相關的用法有以下幾個:

例如:

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 爲我們提供瞭如下方法設置期望被調用的次數。

調用順序

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 成我們預期的值從而進行測試。

總結

在日常工作開發中爲代碼編寫單元測試時如何處理代碼中的接口類型是十分常見的問題,本文介紹瞭如何使用gomockmock 相關接口和如何使用gostub工具對全局變量進行打樁。

在下一篇中,我們將更進一步,詳細介紹如何在編寫單元測試時使用更全能的打樁工具——monkey

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/LDHNMil4_QZFUkdCXDIoRQ