Go Wiki: TableDrivenTests

はじめに

良いテストを書くことは簡単ではありませんが、多くの場合、テーブル駆動テストで多くの範囲をカバーできます。各テーブルエントリは、入力と期待される結果、そしてテスト出力を読みやすくするためのテスト名などの追加情報を含む完全なテストケースです。テストを書くときにコピー&ペーストを使用していることに気づいた場合は、テーブル駆動テストにリファクタリングするか、コピーしたコードをヘルパー関数に抽出する方が良い選択肢ではないかと考えてください。

テストケースのテーブルが与えられると、実際のテストは単にすべてのテーブルエントリを反復処理し、各エントリに必要なテストを実行します。テストコードは一度書かれ、すべてのテーブルエントリに償却されるため、丁寧なエラーメッセージで注意深いテストを書くことが理にかなっています。

テーブル駆動テストは、ツール、パッケージ、その他の何でもなく、単によりクリーンなテストを書くための方法と視点にすぎません。

テーブル駆動テストの例

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の呼び出しはアサーションではありません。エラーが記録された後もテストは続行されます。たとえば、整数入力を伴う何かをテストする場合、関数がすべての入力で失敗するのか、奇数入力のみで失敗するのか、または2の累乗で失敗するのかを知る価値があります。

テストケースを保存するためのマップの使用

前の例では、テストケースは構造体のスライスに保存されていました。それらはマップにも保存でき、この方法にはいくつかの利点があります。

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)
    }
  })
}

マップを使用する利点の1つは、各テストの「名前」を単にマップインデックスにできることです。

さらに重要なのは、マップの反復順序は指定されておらず、ある反復から次の反復まで同じであることも保証されていません。これにより、各テストが他のテストから独立しており、テスト順序が結果に影響を与えないことが保証されます。

並列テスト

テーブルテストの並列化は簡単ですが、バグを回避するために正確さが必要です。以下の3つの変更点、特に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の一部です。