一文搞懂 Go subtest

本文永久鏈接 [2] - https://tonybai.com/2023/03/15/an-intro-of-go-subtest

單元測試 (unit testing)[3] 是軟件開發中至關重要的一環,它存在的意義包括但不限於如下幾個方面:

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 最佳實踐的表驅動 (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 + 表驅動的測試在簡化測試代碼編寫的同時,也會帶來一些不足:

爲此 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 對比着看):

綜上,subtest 的優點可以總結爲以下幾點:

四. Subtest vs. top-level test

top-level test 自身其實也是一種 subtest,只是在它的調度與執行是由 Go 測試框架掌控的的,對我們開發人員並不可見。

對於 gopher 而言:

注:本文少部分內容來自於 ChatGPT[9] 生成的答案。

本文涉及的源碼可以在這裏 [10] 下載。


Gopher Daily(Gopher 每日新聞) 歸檔倉庫 - https://github.com/bigwhite/gopherdaily

我的聯繫方式:

商務合作方式:撰稿、出書、培訓、在線課程、合夥創業、諮詢、廣告合作。

參考資料

[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