Go言語でテストを書く際のベストプラクティスとして、テーブル駆動テスト(Table dirven tests) というのが推奨されている。ようはデータとふるまいを分離しましょうという話で、正直わざわざ名前をつけるようなものでもなかろうという気持ちもないではないが、まあ話がはやくていいね。
けどみんなほんとにこれで満足してるの? と疑問に思うところはある。テストが落ちたときに表示される行番号がテストケースによらず一定で、どのテストが落ちたのかを探すのに一手間かかってしまう。
たとえば以下のコードをテストする際、
package eg import "testing" func TestExample(t *testing.T) { testcases := []struct { name string a, b int sum int }{ {"1+1", 1, 1, 99}, {"2+2", 2, 2, 4}, {"4+4", 4, 4, 8}, // long lines of code... {"1024+1024", 1024, 1024, -1}, } for _, testcase := range testcases { t.Run(testcase.name, func(t *testing.T) { if got, expected := testcase.a+testcase.b, testcase.sum; got != expected { t.Errorf("expected %d, got %d", expected, got) } }) } }
いくつかのテストケースが失敗すると以下のように出力されるが、どれも示されている箇所が同じなのでデータを確認しづらい。一応テストケースに名前をつけて区別することはできているが、ふだんテストが落ちたときに見るところ(ファイル名や行番号)と違うのでやりづらい。
--- FAIL: TestExample (0.00s) --- FAIL: TestExample/1+1 (0.00s) eg_test.go:100: expected 99, got 2 --- FAIL: TestExample/1024+1024 (0.00s) eg_test.go:100: expected -1, got 2048 // 全部同じ行になっちゃうんだよ!!!
motemen/go-tesutil/dataloc
そこで作ったのが go-testutil/dataloc というライブラリ。
dataloc package - github.com/motemen/go-testutil/dataloc - Go Packages
Exampleを見てもらえればわかりそうですが使い方は非常に簡単で、dataloc.L(name string) string
という関数があるのみです。先ほどの例であれば
@@ -1,6 +1,7 @@ package eg import "testing" +import "github.com/motemen/go-testutil/dataloc" func TestExample(t *testing.T) { testcases := []struct { @@ -96,7 +97,7 @@ for _, testcase := range testcases { t.Run(testcase.name, func(t *testing.T) { if got, expected := testcase.a+testcase.b, testcase.sum; got != expected { - t.Errorf("expected %d, got %d", expected, got) + t.Errorf("expected %d, got %d, test case at %s", expected, got, dataloc.L(testcase.name)) } }) }
のように変更すると、
--- FAIL: TestExample (0.00s) --- FAIL: TestExample/1+1 (0.00s) eg_test.go:100: expected 99, got 2, test case at eg_test.go:12 --- FAIL: TestExample/1024+1024 (0.00s) eg_test.go:100: expected -1, got 2048, test case at eg_test.go:95
とテストケースの出自が表示されるわけですね。めでたしめでたし。さっそく今日からご利用ください。
仕組み
中ではとうぜんマジカルなことをしているわけですが、大雑把には runtime.Caller
で呼び出し元のファイルを特定し、go/ast
のASTにしてから以下のようなことをしています。
- dataloc.L(testcase.name) の形式の呼び出しを特定
- testcase の元になっている for ... range testcases を特定
- testcases が定義されている箇所を特定
- 上記の定義で、name が testcase.name の実行時の値("1+1"など)に等しいものを特定
- なのでテストケースの名前が静的に決まっていない場合は特定できません
実行速度でネックになりそうなので型の解析はせず、構文木の情報のみを使ってます。 こんなケースで使えなかったぞってのがあれば教えてくださいね。