メインコンテンツにスキップ
モジュール 9 · レッスン 2 / 5コース内の 42/50~14 min
モジュールのレッスン (2/5)

テーブル駆動テスト

テーブル駆動テスト は Go の慣用的なパターンです。N 個の個別の TestThisSpecificCase 関数を記述する代わりに、ケースのスライスを宣言して反復します。アサーション ブロックは 1 つあり、テスト データはロジックから分離されています。

基本パターン

Go
func TestAbs(t *testing.T) {
    cases := []struct {
        name string
        in   int
        want int
    }{
        {"pos", 3, 3},
        {"neg", -3, 3},
        {"zero", 0, 0},
    }
    for _, tc := range cases {
        t.Run(tc.name, func(t *testing.T) {
            if got := Abs(tc.in); got != tc.want {
                t.Errorf("Abs(%d) = %d; voglio %d", tc.in, got, tc.want)
            }
        })
    }
}

3 つの成分:

  1. 通常のフィールドを含む 匿名構造体のスライス: name、入力、予期される出力 (およびおそらく予期されるエラー)。
  2. for _, tc := range cases — 実行ループ。
  3. t.Run(tc.name, func(t *testing.T) { ... }) — すべてのケースは独自の名前を持つサブテストになります。

なぜ t.Run なのか?

t.Run は、以下を含む サブテストを作成します。

  • 出力に表示される個人名: TestAbs/pos、TestAbs/neg、...
  • 独自の t *testing.T: あるケースで障害が発生しても、他のケースは停止しません (t.Fatal が使用されない限り)。
  • コマンドライン フィルタリング: go test -run TestAbs/neg はその場合にのみ実行されます。
  • オプションの並列処理: 関数内で t.Parallel() を呼び出して並列化します。
Bash
go test -v -run TestAbs/neg

予想されるエラーが発生するケース

Go
cases := []struct {
    name    string
    in      string
    want    int
    wantErr bool
}{
    {"ok", "42", 42, false},
    {"bad", "x", 0, true},
}
for _, tc := range cases {
    t.Run(tc.name, func(t *testing.T) {
        got, err := Atoi(tc.in)
        if (err != nil) != tc.wantErr {
            t.Fatalf("err = %v, wantErr = %v", err, tc.wantErr)
        }
        if !tc.wantErr && got != tc.want {
            t.Errorf("got %d, want %d", got, tc.want)
        }
    })
}

ループ クロージャの落とし穴 (Go 1.22 以前)

Go 1.22 より前のバージョンでは、tc 変数は反復間で共有されていました。t.Run 内で t.Parallel() を呼び出すと、すべてのサブテストが同じ最後のケースに対して実行されました。修正内容は次のとおりです。

Go
for _, tc := range cases {
    tc := tc // local rebind (pre-1.22)
    t.Run(tc.name, func(t *testing.T) {
        t.Parallel()
        // ...
    })
}

ケースに名前を付ける

ベストプラクティス:

  • 短いですが刺激的な名前: "empty input"、"too large"、"negative"。
  • -run を使用して簡単にフィルタリングする場合はスペースを使用しないでください (またはアンダースコアを使用します)。
  • 「ハッピー パス」ケース、「エッジ」ケース (ゼロ、空)、および少なくとも 1 つのエラー ケースを常に含めます。

演習

運動#go.m9.l2.e1
試行回数: 0読み込み中…

in および want のフィールドを含む匿名構造体の case スライスを定義し、Abs (正、負、ゼロ) をテストする 3 つのケースを入力します。

エディターを読み込み中…

3 回の試行後に解決策が利用可能になります

運動#go.m9.l2.e2
試行回数: 0読み込み中…

t.Run with tc.name を追加して、各ケースを名前付きサブテストに変換します。

エディターを読み込み中…

3 回の試行後に解決策が利用可能になります

Quiz#go.m9.l2.e3
準備完了

N 個の個別のテスト関数と比較したテーブル駆動パターンの主な利点は何ですか?

Go
for _, tc := range cases {
  t.Run(tc.name, func(t *testing.T) { ... })
}
回答の選択肢