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的自測