DeNA Testing Blog

Make Testing Fun, Smart, and Delighting End-Users

Golangでtestingことはじめ(2)〜テストにおける共通処理〜

こんにちは。
前回からGolangのテストについて紹介をしています。
今回の記事はその2回目(テストにおける共通処理)に当たります。

この文章中に登場するサンプルは 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

全てのテストケースの前と後に実行するBeforeAllAfterAllを実現したい場合、

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.MRun を呼ぶことで各テストケースが実行され、失敗か成功かに応じてコードを返却します。 最終的に os.Exit0 が渡ればそのテストファイルは成功、それ以外の値の場合は失敗になります。

Run の前後に処理を挟むことでBeforeAllAfterAllが実現できます。

$ 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では少し工夫が必要でした。
しかし、シンプルに記述することができ処理の流れが分かりやすいようになっていると思います。

次回はアプリケーションやツールのテストで利用できる機能(gomockhttptest)について紹介します。