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つは、各テストの「名前」がマップインデックスになることです。

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

並列テスト

テーブルテストを並列化するのは簡単ですが、バグを回避するには正確さが必要です。特にtestの再宣言である、以下の3つの変更点に注意してください。

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の一部です。