t.Run でサブテストを整理する
テーブル駆動テストと組み合わせて使われることが多いのが t.Run によるサブテストだ。各テストケースに名前を付けて独立したサブテストとして実行することで、失敗箇所の特定が容易になり、特定のケースだけを再実行することもできるようになる。
サブテストの基本
t.Run は第1引数にサブテスト名、第2引数にテスト関数を受け取る。テーブル駆動テストのループ内で使うのが典型的なパターンだ。
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
want int
}{
{"positive", 2, 3, 5},
{"zero", 0, 0, 0},
{"negative", -1, -2, -3},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Add(tt.a, tt.b)
if got != tt.want {
t.Errorf("Add(%d, %d) = %d, want %d",
tt.a, tt.b, got, tt.want)
}
})
}
}go test -v で実行すると、各サブテストが独立して表示される。
=== RUN TestAdd
=== RUN TestAdd/positive
=== RUN TestAdd/zero
=== RUN TestAdd/negative
--- PASS: TestAdd (0.00s)
--- PASS: TestAdd/positive (0.00s)
--- PASS: TestAdd/zero (0.00s)
--- PASS: TestAdd/negative (0.00s)TestAdd/positive のように親テスト名とサブテスト名がスラッシュで連結される。この階層構造が -run フラグでの絞り込みに活きてくる。
t.Run を使わない場合との違い
t.Run を使わずにループ内で直接 t.Errorf を呼ぶ書き方と比較すると、いくつかの明確な違いがある。
失敗しても全ケースが実行される。ただし、どのケースが失敗したかはエラーメッセージに依存する
各ケースが独立したサブテストになり、-run フラグで個別に実行できる。t.Fatalf も安全に使える
特に重要なのは t.Fatalf の挙動だ。t.Run を使わずにループ内で t.Fatalf を呼ぶと、テスト関数全体が中断され、後続のケースが実行されない。t.Run の中で t.Fatalf を呼べば、そのサブテストだけが中断され、他のケースは通常通り実行される。
func TestParse(t *testing.T) {
tests := []struct {
name string
input string
want int
}{
{"valid", "42", 42},
{"invalid", "abc", 0},
{"empty", "", 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Parse(tt.input)
if err != nil {
t.Fatalf("Parse(%q) returned error: %v",
tt.input, err)
}
if got != tt.want {
t.Errorf("Parse(%q) = %d, want %d",
tt.input, got, tt.want)
}
})
}
}"invalid" のケースで t.Fatalf が呼ばれても、"empty" のサブテストは問題なく実行される。
-run フラグでサブテストを絞り込む
サブテストの大きな利点は、-run フラグで特定のケースだけを実行できることだ。スラッシュ区切りで親テストとサブテストを指定する。
# TestAdd の positive サブテストだけ実行
go test -v -run TestAdd/positive
# "negative" を含むすべてのサブテストを実行
go test -v -run /negativeデバッグ中に失敗するケースだけを繰り返し実行したい場面で非常に便利だ。テーブル駆動テストのケースが数十個ある場合でも、問題のあるケースにピンポイントでアクセスできる。
クロージャ変数のキャプチャに注意
t.Run と range ループを組み合わせるとき、Go 1.21 以前ではループ変数のキャプチャに注意が必要だった。
// Go 1.21 以前で問題になるパターン
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// tt はループの最後の値を参照してしまう可能性がある
check(t, tt.a, tt.b, tt.want)
})
}Go 1.22 以降ではループ変数のセマンティクスが変更され、各イテレーションで新しい変数が作られるようになったため、この問題は解消されている。ただし、Go 1.21 以前をサポートする必要がある場合はローカル変数にコピーする対策が必要になる。
// Go 1.21 以前での安全な書き方
for _, tt := range tests {
tt := tt // ローカルにコピー
t.Run(tt.name, func(t *testing.T) {
check(t, tt.a, tt.b, tt.want)
})
}ループ変数は各イテレーションでスコープが切られるため、キャプチャの問題は発生しない。
tt := tt のイディオムでローカル変数にシャドウイングする必要がある。
サブテストのネスト
t.Run はネストすることもできる。テストの論理的なグループ分けに使えるが、深くネストしすぎると可読性が落ちるため、2階層程度に留めるのが実践的だ。
func TestMath(t *testing.T) {
t.Run("Add", func(t *testing.T) {
t.Run("positive", func(t *testing.T) {
if Add(1, 2) != 3 {
t.Error("failed")
}
})
t.Run("negative", func(t *testing.T) {
if Add(-1, -2) != -3 {
t.Error("failed")
}
})
})
t.Run("Multiply", func(t *testing.T) {
if Multiply(3, 4) != 12 {
t.Error("failed")
}
})
}実行すると TestMath/Add/positive のような3階層のパスが表示される。関連するテストをまとめたい場合に有効だが、多くの場合はテーブル駆動テスト + t.Run の1階層で十分にカバーできる。
サブテストは Go のテストを構造化するための基本的な道具であり、テーブル駆動テストと合わせて使うことで、保守しやすく読みやすいテストコードが実現できる。