1. 程式人生 > 實用技巧 >go單元測試實踐總結

go單元測試實踐總結

go test基礎用法拾遺

單元測試檔案

TestMain

func TestMain(m *testing.M) {
	os.Exit(m.Run())
}

一個目錄下所有單元測試檔案中只能有一個TestMain函式

執行go test時, 先執行TestMain, 執行至m.Run()時再執行具體的單元測試用例, 環境的初始化和資源釋放等可以放在TestMain裡執行。

需要注意最好使用m.Run()的返回值作為程序退出的返回碼 否則即使對存在單元測試用例不通過的情況也是返回錯誤碼0, 如果是shell指令碼需要用到這個返回值做一些判斷則得不到想要的結果

SubTests

測試某個方法時, 使用子測試的函式來區分不同場景用例,能使單測的程式碼具有更好的閱讀性和維護性。這種場景更推薦table-driven tests的寫法

//  calc_test.go
func TestMul(t *testing.T) {
	cases := []struct {
		Name           string
		A, B, Expected int
	}{
		{"pos", 2, 3, 6},
		{"neg", 2, -3, -6},
		{"zero", 2, 0, 0},
	}

	for _, c := range cases {
		t.Run(c.Name, func(t *testing.T) {
			if ans := Mul(c.A, c.B); ans != c.Expected {
				t.Fatalf("%d * %d expected %d, but %d got",
					c.A, c.B, c.Expected, ans)
			}
		})
	}
}

go test命令

執行指定方法(正則匹配)

go test --run Test_Method1
指定指定方法的指定subtest

用 / 分隔方法名和subtest的name

go test --run Test_Method1/subtest_name

執行多個包的單元測試

執行路徑下所有單元測試(...表示遞迴)
go test ./...
執行指定路徑單元測試(空格分隔)
go test ./pkg/... ./pkg1 ./pkg2

輸出程式碼覆蓋率以及生產程式碼覆蓋率統計檔案

預設統計所有執行到的包的覆蓋率
go test --coverprofile=cp.out
指定需要統計的包(多個路徑用逗號隔開)
go test coverpkg=./pkg/...,./pkg1,./pkg2

格式化顯示程式碼覆蓋率

html
go tool cover --html=cp.out
json
go tool cover --json=cp.out
text
go tool cover --test=cp.out

第三方庫

這些第三方庫或需根據專案和開發的自身情況進行一定的封裝。

table-driven tests程式碼自動生成

https://github.com/cweill/gotests

不過實際使用時感覺也有不靈活之處, 如果有需要也可以直接使用

  • go/build
  • go/ast

自己寫一個專案程式碼分析+程式碼生成的二進位制工具

資料庫mock

https://github.com/DATA-DOG/go-sqlmock

生成的一個mocker物件用來執行對db的mock設定, 與此同時額外生成一個官方庫sql.DB的interface。這個interface的行為受mocker物件控制(query和exec的返回結果)。

只要orm的庫操作db的時候, 是通過sql.DB操作, 只要將go-sqlmock替換掉真正連線資料庫的sql.DB即可。當然也需要ORM庫提供使用sql.DB初始化的方法(如GORM v1,v2都支援)

實際使用時, 應該還需要根據自己的orm以及需求對該包做一些簡單的封裝。且資料用例應該是可以複用的, 這些資料用例建議以go的model層的結構體或者檔案的形式維護, 再做相應的讀取封裝。 目前筆者的實踐是以結構體的形式維護, 方便程式碼跳轉。

redis mock

https://github.com/alicebob/miniredis

相比sqlmock的mock方式, 這個第三方包的mock方式就簡單粗暴一點, 直接在記憶體裡起一個基本支援redis所有命令的迷你的redis服務端(會佔用一個埠)。 在進行單元測試時, 將客戶端的連線指向這個迷你的redis伺服器即可。

所有的redis命令也可以通過啟動mini redis server時生成的*miniredis.Server來操作, 增刪需要的kv

任意interface的mock

https://github.com/golang/mock

由於go不支援動態給struct增加/修改方法, 所以要mock一個interface, 只能新增一個實現了該interface的struct。

go官方提供的這個第三方包提供了mockgen這個根據interface生成mock struct程式碼的二進位制檔案, 單元測試時使用mockgen生成的struct即可。

斷言

https://github.com/stretchr/testify

該包還提供了其他功能

斷言使用的是github.com/stretchr/testify/assert

對*testing.T一些方法的封裝, 方便對被測試方法的各種型別結果進行斷言並在斷言失敗時友好地輸出錯誤資訊

個人Go單元測試實踐總結

  • 一般table-driven例子裡, 是直接指定的輸出結果, 然後對方法返回值做相等校驗。 然而實際上如果是web業務常會有一些函式返回的資料較多, 如果期望結果強校驗或許有些不便以及不靈活, 可以讓用例提供斷言函式(入參中有被測方法返回值), 根據用例自定義該對結果斷言(可以是全等)
  • 為了程式碼的可單元測試, 業務程式碼中外部依賴IO時, 除非有go-sqlmock, miniredis這樣方便的mock庫,否則使用interface方式解耦(如訊息佇列的客戶端, grpc客戶端等)
  • 為了減少對路徑的依賴, 配置檔案的資料結構可以單獨為單元測試寫一個不依賴於配置檔案的初始化函式。
  • 全域性環境, 資料結構, 配置的初始化等抽到一個函式, 放在TestMain裡做
  • 可以在專案中維護一個統計專案級別單元測試覆蓋率, 用例是否通過等的shell指令碼,用來方便統計檢視專案的單元測試情況以及提供給CI/CD使用
  • 單元測試的重點應該是分支的覆蓋率而非引數的邊界性
  • 開發時可以以單元測試驅動開發, 定義好能走到各個分支的單元測試用例。開發介面/函式時先走通單元測試用例再進行真正介面級別依賴外部IO的自測