一文搞懂 Go subtest
本文永久鏈接 [2] - https://tonybai.com/2023/03/15/an-intro-of-go-subtest
單元測試 (unit testing)[3] 是軟件開發中至關重要的一環,它存在的意義包括但不限於如下幾個方面:
-
提高代碼質量:單元測試可以確保代碼的正確性、可靠性和穩定性,從而減少代碼缺陷和 bug。
-
減少迴歸測試成本:在修改代碼時,單元測試可以快速檢查是否影響了其他模塊的功能,避免了整個系統的迴歸測試成本。
-
促進團隊合作:單元測試可以幫助團隊成員更好地理解和使用彼此編寫的代碼,提高代碼的可讀性和可維護性。
-
提高開發效率:單元測試可以自動化執行測試,減少手動測試的時間和工作量,從而提高開發效率。
Go 語言設計者在 Go 設計伊始就決定語言特性與環境特性 “兩手都要抓,兩手都要硬”,事實證明:Go 的成功正是因爲其對工程軟件項目整體環境的專注 [4]。而 Go 內置輕量級測試框架這一點也正是 Go 重視環境特性的體現。並且,Go 團隊對這一內置測試框架的投入是持續的,不斷有更便捷的、更靈活的新特性加入 Go 測試框架中,可以幫助 Gopher 更好地組織測試代碼,更高效地執行測試等。
Go 在 Go 1.7 版本 [5] 引入的 subtest 就是一個典型的代表,subtest 的加入使得 Gopher 可以更靈活地應用內置 go test 框架。
在本文中,我將結合日常開發中瞭解到的關於 subtest 的認知、理解和使用的問題,和大家一起聊聊 subtest。
一. Go 單元測試回顧
在 Go 語言中,單元測試被視爲一等公民,結合 Go 內置的輕量級測試框架,Go 開發者可以很方便的編寫單元測試用例。
Go 的單元測試通常放在與被測試代碼相同的包中,單元測試所在源文件以_test.go 結尾,這個 Go 測試框架要求的。測試函數以 Test 爲前綴,接受一個 * testing.T 類型的參數,並使用 t.Error、t.Fail 以及 t.Fatal 等方法來報告測試失敗。使用 go test 命令即可運行所有的測試代碼。如果測試通過,則輸出一條消息表示測試成功;否則輸出錯誤信息,指出哪些測試失敗了。
注:Go 還支持基準測試、example 測試、模糊測試 [6] 等,以便進行性能測試和文檔生成,但這些不是這篇文章所要關注的內容。 注:t.Error <=> t.Log+t.Fail
通常編寫 Go 測試代碼時,我們首先會考慮 top-level test。
二. Go top-level test
上面提到的與被測源碼在相同目錄下的 *_test.go 中的以 Test 開頭的函數就是 Go top-level test。在 *_test.go 可以定義一個或多個以 Test 開頭的函數用於測試被測源碼中函數或方法。例如:
// https://github.com/bigwhite/experiments/blob/master/subtest/add_test.go
// 被測代碼,僅是demo
func Add(a, b int) int {
return a + b
}
// 測試代碼
func TestAdd(t *testing.T) {
got := Add(2, 3)
if got != 5 {
t.Errorf("Add(2, 3) got %d, want 5", got)
}
}
func TestAddZero(t *testing.T) {
got := Add(2, 0)
if got != 2 {
t.Errorf("Add(2, 0) got %d, want 2", got)
}
}
func TestAddOppositeNum(t *testing.T) {
got := Add(2, -2)
if got != 0 {
t.Errorf("Add(2, -2) got %d, want 0", got)
}
}
注:“got-want” 是 Go test 中在 Errorf 中常用的命名慣例
top-level test 的執行有如下特點:
-
go test 會將每個 TestXxx 放在單獨的 goroutine 中執行,保持相互的隔離;
-
某個 TestXxx 用例未過,通過 Errorf,甚至是 Fatalf 輸出錯誤結果,都不會影響到其他 TestXxx 的執行;
-
某個 TestXxx 用例中的某個結果判斷未過,如果通過 Errorf 輸出錯誤結果,則該 TestXxx 會繼續執行;
-
不過,如果 TestXxx 使用的是 Fatal/Fatalf,這會導致該 TestXxx 的執行在調用 Fatal/Fatalf 的位置立即結束,TestXxx 函數體內的後續測試代碼將不會得到執行;
-
默認各個 TestXxx 按聲明順序逐一執行,即便它們是在各自的 goroutine 中執行的;
-
通過 go test -shuffle=on 可以讓各個 TestXxx 按隨機次序執行,這樣可以檢測出各個 TestXxx 之間是否存在執行順序的依賴,我們要避免在測試代碼中出現這種依賴;
-
通過 “go test -run = 正則式” 的方式,可以選擇執行某些 TestXxx。
-
各個 TestXxx 函數可以調用 t.Parallel 方法 (即 testing.T.Parallel 方法) 來將 TestXxx 加入到可並行執行的用例集合中,注意:加入到並行執行集合後,這些 TestXxx 的執行順序就不確定了。
結合屬於 Go 最佳實踐的表驅動 (table-driven) 測試(如下面代碼 TestAddWithTable 所示),我們可以無需寫很多 TestXxx,用下面的 TestAddWithTable 即可實現上面三個 TestXxx 的等價測試:
func TestAddWithTable(t *testing.T) {
cases := []struct {
name string
a int
b int
r int
}{
{"2+3", 2, 3, 5},
{"2+0", 2, 0, 2},
{"2+(-2)", 2, -2, 0},
//... ...
}
for _, caze := range cases {
got := Add(caze.a, caze.b)
if got != caze.r {
t.Errorf("%s got %d, want %d", caze.name, got, caze.r)
}
}
}
Go top-level test 可以滿足大多數 Gopher 的常規單測需求,表驅動的慣例理解起來也十分容易。
但基於 top-level test + 表驅動的測試在簡化測試代碼編寫的同時,也會帶來一些不足:
-
表內的 cases 順序執行,無法 shuffle;
-
表內所有 cases 在同一個 goroutine 中執行,隔離性差;
-
如果使用 fatal/fatalf,那麼一旦某個 case 失敗,後面的測試表項 (cases) 將不能得到執行;
-
表內 test case 無法並行 (parallel) 執行;
-
測試用例的組織只能平鋪,不夠靈活,無法建立起層次。
爲此 Go 1.7 版本 [7] 引入了 subtest!
三. Subtest
Go 語言的 subtest 是指將一個測試函數 (TestXxx) 分成多個小測試函數,每個小測試函數可以獨立運行並報告測試結果的功能。這種測試方式可以更細粒度地控制測試用例,方便定位問題和調試。
下面是一個使用 subtest 改造 TestAddWithTable 的示例代碼,展示如何使用 Go 語言編寫 subtest:
// https://github.com/bigwhite/experiments/blob/master/subtest/add_sub_test.go
func TestAddWithSubtest(t *testing.T) {
cases := []struct {
name string
a int
b int
r int
}{
{"2+3", 2, 3, 5},
{"2+0", 2, 0, 2},
{"2+(-2)", 2, -2, 0},
//... ...
}
for _, caze := range cases {
t.Run(caze.name, func(t *testing.T) {
t.Log("g:", curGoroutineID())
got := Add(caze.a, caze.b)
if got != caze.r {
t.Errorf("got %d, want %d", got, caze.r)
}
})
}
}
在上面的代碼中,我們定義了一個名爲 TestAddWithSubtest 的測試函數,並在其中使用 t.Run() 方法結合表測試方式來創建三個 subtest,這樣每個 subtest 都可以複用相同的錯誤處理邏輯,但通過測試用例參數的不同來體現差異。當然你若不使用表驅動測試,那麼每個 subtest 也都可以有自己獨立的錯誤處理邏輯!
執行上面 TestAddWithSubtest 這個測試用例(我們故意將 Add 函數的實現改成錯誤的),我們將看到下面結果:
$go test add_sub_test.go
--- FAIL: TestAddWithSubtest (0.00s)
--- FAIL: TestAddWithSubtest/2+3 (0.00s)
add_sub_test.go:54: got 6, want 5
--- FAIL: TestAddWithSubtest/2+0 (0.00s)
add_sub_test.go:54: got 3, want 2
--- FAIL: TestAddWithSubtest/2+(-2) (0.00s)
add_sub_test.go:54: got 1, want 0
我們看到:在錯誤信息輸出中,每個失敗 case 都是以 “TestXxx/subtestName” 標識,我們可以很容易地將其與相應的代碼行對應起來。更深層的意義是 subtest 讓整個測試組織形式有了 “層次感”!通過 - run 標誌位,我們便能夠以這種“層次” 選擇要執行的某個 top-level test 的某個 / 某些 Subtest:
$go test -v -run TestAddWithSubtest/-2 add_sub_test.go
=== RUN TestAddWithSubtest
=== RUN TestAddWithSubtest/2+(-2)
add_sub_test.go:51: g: 19
add_sub_test.go:54: got 1, want 0
--- FAIL: TestAddWithSubtest (0.00s)
--- FAIL: TestAddWithSubtest/2+(-2) (0.00s)
FAIL
FAIL command-line-arguments 0.006s
FAIL
我們來看看 subtest 有哪些特點 (可以和前面的 top-level test 對比着看):
-
go subtest 也會放在單獨的 goroutine 中執行,保持相互的隔離;
-
某個 Subtest 用例未過,通過 Errorf,甚至是 Fatalf 輸出錯誤結果,都不會影響到同一 TestXxx 下的其他 Subtest 的執行;
-
某個 Subtest 中的某個結果判斷未過,如果通過 Errorf 輸出錯誤結果,則該 Subtest 會繼續執行;
-
不過,如果 subtest 使用的是 Fatal/Fatalf,這會導致該 subtest 的執行在調用 Fatal/Fatalf 的位置立即結束,subtest 函數體內的後續測試代碼將不會得到執行;
-
默認各個 TestXxx 下的 subtest 將按聲明順序逐一執行,即便它們是在各自的 goroutine 中執行的;
-
到目前爲止,subtest 不支持 shuffle 方式的隨機序執行;
-
通過 “go test -run=TestXxx / 正則式[/...]” 的方式,我們可以選擇執行 TestXxx 下的某個或某些 subtest;
-
各個 subtest 可以調用 t.Parallel 方法 (即 testing.T.Parallel 方法) 來將 subtest 加入到可並行執行的用例集合中,注意:加入到並行執行集合後,這些 subTest 的執行順序就不確定了。
綜上,subtest 的優點可以總結爲以下幾點:
-
更細粒度的測試:通過將測試用例分成多個小測試函數,可以更容易地定位問題和調試。
-
可讀性更好:subtest 可以讓測試代碼更加清晰和易於理解。
-
更靈活的測試:subtest 可以根據需要進行組合和排列,以滿足不同的測試需求。
-
更有層次的組織測試代碼:通過 subtest 可以設計出更有層次的測試代碼組織形式,更方便地共享資源和在某一組織層次上設置 setup 與 teardown,我的《Go 語言精進之路》vol2[8] 的第 41 條 “有層次地組織測試代碼” 對這方面內容有系統說明,大家可以參考。
四. Subtest vs. top-level test
top-level test 自身其實也是一種 subtest,只是在它的調度與執行是由 Go 測試框架掌控的的,對我們開發人員並不可見。
對於 gopher 而言:
-
簡單的包的測試在 top-level test 中就可以滿足,直接、直觀、易懂。
-
對於稍大一些的工程中的複雜包來說,一旦涉及到測試代碼組織的層次設計時,subtest 的組織性、靈活性和可擴展性便可以更好地的幫助我們提高測試效率和減少測試時間。
注:本文少部分內容來自於 ChatGPT[9] 生成的答案。
本文涉及的源碼可以在這裏 [10] 下載。
Gopher Daily(Gopher 每日新聞) 歸檔倉庫 - https://github.com/bigwhite/gopherdaily
我的聯繫方式:
-
微博 (暫不可用):https://weibo.com/bigwhite20xx
-
微博 2:https://weibo.com/u/6484441286
-
博客:tonybai.com
-
github: https://github.com/bigwhite
商務合作方式:撰稿、出書、培訓、在線課程、合夥創業、諮詢、廣告合作。
參考資料
[1]
lexica AI: https://lexica.art/
[2]
本文永久鏈接: https://tonybai.com/2023/03/15/an-intro-of-go-subtest
[3]
單元測試 (unit testing): https://tonybai.com/2010/09/30/opensource-a-lightweight-c-unit-test-framework/
[4]
Go 的成功正是因爲其對工程軟件項目整體環境的專注: https://tonybai.com/2022/05/04/the-paper-of-go-programming-language-and-environment
[5]
Go 1.7 版本: https://tonybai.com/2016/06/21/some-changes-in-go-1-7
[6]
模糊測試: https://tonybai.com/2021/12/01/first-class-fuzzing-in-go-1-18
[7]
Go 1.7 版本: https://tonybai.com/2016/06/21/some-changes-in-go-1-7
[8]
《Go 語言精進之路》vol2: https://book.douban.com/subject/35720729/
[9]
ChatGPT: https://openai.com/blog/chatgpt
[10]
這裏: https://github.com/bigwhite/experiments/blob/master/subtest
[11]
“Gopher 部落” 知識星球: https://wx.zsxq.com/dweb2/index/group/51284458844544
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/5zoNwCWnNNunrDtu6cokqg