Go Wiki: 表驅動測試

引言

編寫良好的測試並非易事,但在許多情況下,表驅動測試可以覆蓋大部分內容:每個表條目都是一個完整的測試用例,包含輸入和預期結果,有時還包含其他資訊,如測試名稱,以便測試輸出易於閱讀。如果您發現自己在編寫測試時使用了複製貼上,請考慮重構為表驅動測試或將複製的程式碼提取到輔助函式中是否是更好的選擇。

給定一個測試用例表,實際測試只是遍歷所有表條目,併為每個條目執行必要的測試。測試程式碼編寫一次,並分攤到所有表條目上,因此編寫帶有良好錯誤訊息的仔細測試是值得的。

表驅動測試不是一個工具、包或其他什麼,它只是一種編寫更清晰測試的方式和視角。

表驅動測試示例

這裡有一個來自 fmt 包測試程式碼中的好例子 ( https://pkg.go.dev/fmt/ )

var flagtests = []struct {
    in  string
    out string
}{
    {"%a", "[%a]"},
    {"%-a", "[%-a]"},
    {"%+a", "[%+a]"},
    {"%#a", "[%#a]"},
    {"% a", "[% a]"},
    {"%0a", "[%0a]"},
    {"%1.2a", "[%1.2a]"},
    {"%-1.2a", "[%-1.2a]"},
    {"%+1.2a", "[%+1.2a]"},
    {"%-+1.2a", "[%+-1.2a]"},
    {"%-+1.2abc", "[%+-1.2a]bc"},
    {"%-1.2abc", "[%-1.2a]bc"},
}
func TestFlagParser(t *testing.T) {
    var flagprinter flagPrinter
    for _, tt := range flagtests {
        t.Run(tt.in, func(t *testing.T) {
            s := Sprintf(tt.in, &flagprinter)
            if s != tt.out {
                t.Errorf("got %q, want %q", s, tt.out)
            }
        })
    }
}

請注意 t.Errorf 提供的詳細錯誤訊息:它顯示了結果和預期結果;輸入是子測試的名稱。當測試失敗時,即使不閱讀測試程式碼,也能立即清楚哪個測試失敗了以及為什麼。

呼叫 t.Errorf 並不是斷言。即使記錄了錯誤,測試也會繼續進行。例如,當測試具有整數輸入的某些內容時,瞭解該函式對所有輸入都失敗,或者僅對奇數輸入失敗,或者對二的冪失敗是值得的。

使用 Map 儲存測試用例

在之前的示例中,測試用例儲存在結構體切片中。它們也可以儲存在 map 中,並且這樣做有幾個優點。

tests := map[string]struct {
  input string
  result string
} {
  "empty string":  {
    input: "",
    result: "",
  },
  "one character": {
    input: "x",
    result: "x",
  },
  "one multi byte glyph": {
    input: "🎉",
    result: "🎉",
  },
  "string with multiple multi-byte glyphs": {
    input: "🥳🎉🐶",
    result: "🐶🎉🥳",
  },
}

for name, test := range tests {
  // test := test // NOTE: uncomment for Go < 1.22, see /doc/faq#closures_and_goroutines
  t.Run(name, func(t *testing.T) {
    t.Parallel()
    if got, expected := reverse(test.input), test.result; got != expected {
      t.Fatalf("reverse(%q) returned %q; expected %q", test.input, got, expected)
    }
  })
}

使用 map 的一個優點是,每個測試的“名稱”可以簡單地作為 map 的索引。

更重要的是,map 的迭代順序未指定,甚至不保證每次迭代都相同。這確保了每個測試都獨立於其他測試,並且測試順序不會影響結果。

並行測試

並行化表測試很簡單,但需要精確操作以避免錯誤。請仔細注意下面的三個更改,特別是 test 的重新宣告。

package main

import (
    "testing"
)

func TestTLog(t *testing.T) {
    t.Parallel() // marks TLog as capable of running in parallel with other tests
    tests := []struct {
        name string
    }{
        {"test 1"},
        {"test 2"},
        {"test 3"},
        {"test 4"},
    }
    for _, test := range tests {
    // test := test // NOTE: uncomment for Go < 1.22, see /doc/faq#closures_and_goroutines
        t.Run(test.name, func(t *testing.T) {
            t.Parallel() // marks each test case as capable of running in parallel with each other 
            t.Log(test.name)
        })
    }
}

參考


此內容是 Go Wiki 的一部分。