こんにちは。
前回からGolangのテストについて紹介をしています。
今回の記事はその2回目(テストにおける共通処理)に当たります。
- testingパッケージを使ったユニットテスト(testing)
- テストにおける共通処理(testing)
- アプリケーションのテスト(gomock, httptest)
この文章中に登場するサンプルは GitHub にありますので、実際に動作させることが可能です。
$ go get github.com/duck8823/sample-go-testing $ cd $GOPATH/src/github.com/duck8823/sample-go-testing $ git checkout refs/tags/blog $ dep ensure # 依存パッケージのダウンロード
なお、文章中のコマンドは全てバージョン1.9.2
での動作を確認しています。
テストの共通処理
いくつかのテストケースを書いていて、共通処理をまとめたいと思ったことはないでしょうか。
他言語のテスティングフレームワークには、必ずといっていいほどこの機能が提供されています。
BeforeAll / AfterAll
全てのテストケースの前と後に実行するBeforeAll
やAfterAll
を実現したい場合、
func TestMain(m *testing.M)
を利用します。
animals/animals_03_test.go
package animals_test import ( "github.com/duck8823/sample-go-testing/animals" "github.com/duck8823/sample-go-testing/foods" "os" "testing" ) func TestMain(m *testing.M) { println("before all...") code := m.Run() println("after all...") os.Exit(code) } func TestDuck_Say_03(t *testing.T) { duck := animals.NewDuck("tarou") actual := duck.Say() expected := "tarou says quack" if actual != expected { t.Errorf("got: %v\nwant: %v", actual, expected) } } func TestDuck_Eat(t *testing.T) { duck := animals.NewDuck("tarou") apple := foods.NewApple("sunfuji") actual := duck.Eat(apple) expected := "tarou ate sunfuji" if actual != expected { t.Errorf("got: %v\nwant: %v", actual, expected) } }
テストファイル内に TestMain
が存在している場合、 go test
はTestMainのみ実行します。
testing.M
の Run
を呼ぶことで各テストケースが実行され、失敗か成功かに応じてコードを返却します。
最終的に os.Exit
に 0
が渡ればそのテストファイルは成功、それ以外の値の場合は失敗になります。
Run
の前後に処理を挟むことでBeforeAll
とAfterAll
が実現できます。
$ go test -v ./animals/animals_03_test.go before all... === RUN TestDuck_Say_03 --- PASS: TestDuck_Say_03 (0.00s) === RUN TestDuck_Eat --- PASS: TestDuck_Eat (0.00s) PASS after all... ok command-line-arguments 0.007s
BeforeEach / AfterEach
続いてテストケースごとに前処理や後処理を実行する方法です。
ループによるBeforeEachの実現
この方法は可読性も低く、テストケースを指定した場合無駄に処理が行われてしまうので推奨しません。
下記のサンプルコードではループとサブテストを利用し、 BeforeEachを実現しています。
animals/animals_04_test.go
package animals_test import ( "github.com/duck8823/sample-go-testing/animals" "github.com/duck8823/sample-go-testing/foods" "testing" ) func TestDuck_04(t *testing.T) { println("before all...") var duck *animals.Duck for _, testcase := range []struct { name string call func(t *testing.T) }{ { "it says quack", func(t *testing.T) { actual := duck.Say() expected := "tarou says quack" if actual != expected { t.Errorf("got: %v\nwant: %v", actual, expected) } }, }, { "it ate apple", func(t *testing.T) { apple := foods.NewApple("sunfuji") actual := duck.Eat(apple) expected := "tarou ate sunfuji" if actual != expected { t.Errorf("got: %v\nwant: %v", actual, expected) } }, }, } { println("before each...") duck = animals.NewDuck("tarou") // テストケースの実行 t.Run(testcase.name, testcase.call) println("after each...") } println("after all...") }
コードが長くなってしまいましたが、
duck = animals.NewDuck("tarou")
の行ではテストケース毎に変数 duck
を初期化しようとしています。
コード全体を見るとケース名と関数をスライスにいれ、それをループしてサブテストとして実行させています。
これを実行してみると、
$ go test -v ./animals/animals_04_test.go === RUN TestDuck_04 before all... before each... === RUN TestDuck_04/it_says_quack after each... before each... === RUN TestDuck_04/it_ate_apple after each... after all... --- PASS: TestDuck_04 (0.00s) --- PASS: TestDuck_04/it_says_quack (0.00s) --- PASS: TestDuck_04/it_ate_apple (0.00s) PASS ok command-line-arguments 0.007s
テストケースの前後に処理を挟むことができました。
また、--run
オプションで特定のテストケースのみを実行することができます。
ある関数のリファクタリングをする場合など早いフィードバックが欲しい時は、テストケースを指定することで実行時間の短縮が期待できます。
$ go test -v ./animals/animals_04_test.go --run TestDuck_04/it_says_quack === RUN TestDuck_04 before all... before each... === RUN TestDuck_04/it_says_quack after each... before each... after each... after all... --- PASS: TestDuck_04 (0.00s) --- PASS: TestDuck_04/it_says_quack (0.00s) PASS ok command-line-arguments 0.007s
この場合も正しく実行できていますが、before each...
と after each...
が余分にプリントされてしまっています。 --run
オプションでは上から順にテストを実行し、一致しないものをスキップするようです。
スキップしたテストケースについても前処理を実行してしまうため、前処理が重い場合は全体の実行時間が長くなってしまいます。
前処理に5秒のSleepを追加してみます。
animals/animals_04_test.go
--- a/animals/animals_04_test.go +++ b/animals/animals_04_test.go @@ -4,6 +4,7 @@ import ( "github.com/duck8823/sample-go-testing/animals" "github.com/duck8823/sample-go-testing/foods" "testing" + "time" ) func TestDuck_04(t *testing.T) { @@ -41,6 +42,7 @@ func TestDuck_04(t *testing.T) { duck = animals.NewDuck("tarou") // テストケースの実行 + time.Sleep(5 * time.Second) t.Run(testcase.name, testcase.call) println("after each...")
$ go test -v ./animals/animals_04_test.go --run TestDuck_04/it_says_quack === RUN TestDuck_04 before all... before each... === RUN TestDuck_04/it_says_quack after each... before each... after each... after all... --- PASS: TestDuck_04 (10.01s) --- PASS: TestDuck_04/it_says_quack (0.00s) PASS ok command-line-arguments 10.014s
Sleepを追加した結果、全体の実行時間が10秒もかかってしまいました。
これでは、テストケースを指定しても実行時間の短縮になりません。
また、前後処理とテストケース内の処理が分離されてしまいコード全体の可読性も悪くなってしまっています。
関数による共通化
上述の例では、テストケースの外で共通処理を呼び出すとテストケースを指定した場合は無駄に共通処理が行われてしまいました。
各テストケース内で共通化した関数を明示的に呼び出すよう書き換えてみましょう。
animals/animals_05_test.go
package animals_test import ( "github.com/duck8823/sample-go-testing/animals" "github.com/duck8823/sample-go-testing/foods" "testing" "time" ) func TestDuck_05(t *testing.T) { t.Run("it says quack", func(t *testing.T) { duck := createInstance() actual := duck.Say() expected := "tarou says quack" if actual != expected { t.Errorf("got: %v\nwant: %v", actual, expected) } }) t.Run("it ate apple", func(t *testing.T) { duck := createInstance() apple := foods.NewApple("sunfuji") actual := duck.Eat(apple) expected := "tarou ate sunfuji" if actual != expected { t.Errorf("got: %v\nwant: %v", actual, expected) } }) } func createInstance() *animals.Duck { duck := animals.NewDuck("tarou") time.Sleep(5 * time.Second) return duck }
可読性は非常に高くなりました。
また、
$ go test -v ./animals/animals_05_test.go --run TestDuck_05/it_says_quack === RUN TestDuck_05 === RUN TestDuck_05/it_says_quack --- PASS: TestDuck_05 (5.00s) --- PASS: TestDuck_05/it_says_quack (5.00s) PASS ok command-line-arguments 5.011s
実行時間も前処理1回分になりました。
共通メソッドのエラー処理
共通メソッド内でエラーが発生した場合、テストを失敗させる必要があります。
そこで、共通メソッドに testing.TB
インタフェースを渡してやります。
Fatal
をコールすることでテストを失敗にし、その場でそのテストケースを終了させてやることができます。
Error
の場合はテストを失敗にしますが、テストケースは引き続き実行されます。
--- a/animals/animals_05_test.go +++ b/animals/animals_05_test.go @@ -9,7 +9,7 @@ import ( func TestDuck_05(t *testing.T) { t.Run("it says quack", func(t *testing.T) { - duck := createInstance() + duck := createInstance(t) actual := duck.Say() expected := "tarou says quack" @@ -19,7 +19,7 @@ func TestDuck_05(t *testing.T) { }) t.Run("it ate apple", func(t *testing.T) { - duck := createInstance() + duck := createInstance(t) apple := foods.NewApple("sunfuji") @@ -31,9 +31,10 @@ func TestDuck_05(t *testing.T) { }) } -func createInstance() *animals.Duck { +func createInstance(tb testing.TB) *animals.Duck { duck := animals.NewDuck("tarou") time.Sleep(5 * time.Second) + tb.Error("前処理で失敗しました.") return duck }
$ go test -v ./animals/animals_05_test.go === RUN TestDuck_05 === RUN TestDuck_05/it_says_quack === RUN TestDuck_05/it_ate_apple --- FAIL: TestDuck_05 (10.00s) --- FAIL: TestDuck_05/it_says_quack (5.00s) animals_05_test.go:37: 前処理で失敗しました. --- FAIL: TestDuck_05/it_ate_apple (5.00s) animals_05_test.go:37: 前処理で失敗しました. FAIL exit status 1 FAIL command-line-arguments 10.012s
Errorを使っているので残りのテストケースはちゃんと実行されていることが確認できますが、
2つのサブテストにおいて同一箇所で失敗したと表示されてしまいました。
特に1つのケース内で複数回コールされる関数の場合、どこで失敗したか特定しにくくなってしまいます。
これを、ケース内のどこで失敗したかわかるようにしてみます。
共通処理内で testing.TB
インタフェースの Helper
をコールすることで、
呼び出し部分が表示されるようになります。
先ほど変更したソースコードに、さらに変更を加えます。
--- a/animals/animals_05_test.go +++ b/animals/animals_05_test.go @@ -32,6 +32,8 @@ func TestDuck_05(t *testing.T) { } func createInstance(tb testing.TB) *animals.Duck { + tb.Helper() + duck := animals.NewDuck("tarou") time.Sleep(5 * time.Second) tb.Error("前処理で失敗しました")
$ go test -v ./animals/animals_05_test.go === RUN TestDuck_05 === RUN TestDuck_05/it_says_quack === RUN TestDuck_05/it_ate_apple --- FAIL: TestDuck_05 (10.01s) --- FAIL: TestDuck_05/it_says_quack (5.01s) animals_05_test.go:12: 前処理で失敗しました. --- FAIL: TestDuck_05/it_ate_apple (5.00s) animals_05_test.go:22: 前処理で失敗しました. FAIL exit status 1 FAIL command-line-arguments 10.013s
表示されるソースコードの行数が変わっていることがわかります。 こちらの方がテストケース内のどこで失敗したか原因を探りやすくなると思います。
最後に
テストケース毎に実行する前処理や後処理は、Golangでは少し工夫が必要でした。
しかし、シンプルに記述することができ処理の流れが分かりやすいようになっていると思います。
次回はアプリケーションやツールのテストで利用できる機能(gomock
とhttptest
)について紹介します。