TL; DR
t.Run
でネストしたテストを実行する時、Runに渡す関数はトップレベルのテストが実行されているのと別のgoroutineで実行されているのでParameterized Testなんかをやってる場合、変数のスコープに注意しましょう。
詳細
Goでテストをテストケースを書く際に以下のようにパラメータと期待値を指定してforでサブテストを実行する事がよくあると思います。
import ( "fmt" "testing" ) func Add(x, y int) int { return x + y } func TestAdd(t *testing.T) { tests := []struct { x, y int expected int }{ {x: 1, y: 2, expected: 3}, {x: 2, y: 3, expected: 5}, {x: 50, y: 50, expected: 100}, } for _, tc := range tests { t.Run(fmt.Sprintf("%d+%d", tc.x, tc.y), func(t *testing.T) { if actual := Add(tc.x, tc.y); actual != tc.expected { t.Errorf("expected: %d actual: %d", tc.expected, actual) } }) } }
これを高速化のため t.Parallel
を追加します。
func TestAdd(t *testing.T) { t.Parallel() // Add tests := []struct { x, y int expected int }{ {x: 1, y: 2, expected: 3}, {x: 2, y: 3, expected: 5}, {x: 50, y: 50, expected: 100}, } for _, tc := range tests { t.Run(fmt.Sprintf("%d+%d", tc.x, tc.y), func(t *testing.T) { t.Parallel() // Add t.Logf("%+v", tc) if actual := Add(tc.x, tc.y); actual != tc.expected { t.Errorf("expected: %d actual: %d", tc.expected, actual) } }) } }
これを実行すると成功します。
ただし、以下のようにテストで渡されたパラメタを出力してみると、すべて {x:50 y:50 expected:100}
であることが分かります。つまり最初の2つのテストケースは意図したように動いてないのです。
for _, tc := range tests { t.Run(fmt.Sprintf("%d+%d", tc.x, tc.y), func(t *testing.T) { t.Parallel() t.Logf("%+v", tc) // Add if actual := Add(tc.x, tc.y); actual != tc.expected { t.Errorf("expected: %d actual: %d", tc.expected, actual) } }) }
原因と対応
なぜこういう事が起こるかというと、t.Runに渡されたfunctionは別のGoroutineで実行され、 t.Parallel
をつけることで各テスト関数が並列に実行されるため、for文のスコープで初期化されている tc
の値は最後に代入された {x:50 y:50 expected:100}
の値をすべてのfunctionが参照してしまうことになるためです。
これを修正するためにはfor文の中でもう一度 tc
をすればOKです。
for _, tc := range tests { tc := tc // Add t.Run(fmt.Sprintf("%d+%d", tc.x, tc.y), func(t *testing.T) { t.Parallel() t.Logf("%+v", tc) if actual := Add(tc.x, tc.y); actual != tc.expected { t.Errorf("expected: %d actual: %d", tc.expected, actual) } }) }
for文の中で直接goroutineやdeferを実行するときはIDEのIntelliJが注意してくれるので意識できてたのですが、テストに関しては完全に盲点で1時間ほど時間を溶かしてしまいました..orz
参考
Frequently Asked Questions (FAQ) - The Go Programming Language