MicroAd Developers Blog

マイクロアドのエンジニアブログです。インフラ、開発、分析について発信していきます。

GoのsubTestsを活用したTableDrivenTest

こんにちは。 マイクロアドで機械学習エンジニアをしている大庭です。 今回の記事では Go で TableDrivenTest を書く上で重要になってくる subTests の使い方を紹介します。

subTests

subTests は Go1.7 から追加された1つのテスト関数の中に階層を作るための機能です。 subTests を利用するメリットとしてはテストケース毎に正否判定を行える、特定のテストケースのみ実行できる、テストの並列実行ができるなどが挙げられます。

subTests の基本的な使い方

はじめに subTests の基本的な利用方法を subTests ありなしのコードを比較する形で実装例を紹介します。

例として足し算をする関数Addのテストを示します。 テスト失敗時の結果を見るために4つ目のケースは失敗するようにしています。

まずは subTests なしで書いたテストです。

func TestAdd(t *testing.T) {
    tests := []struct {
        a, b, want int
    }{
        {0, 1, 1},
        {1, 2, 3},
        {2, 3, 5},
        {3, 4, 8}, // 失敗するテストケース
    }
    for _, tt := range tests {
        if got := Add(tt.a, tt.b); got != tt.want {
            t.Errorf("got %d, want %d", got, tt.want)
        }
    }
}

go.dev

こちらのテストも Go の TableDrivenTest に則っているので、 テストケースとテストロジックが分離されていて十分読みやすいです。

こちらのテストを実行すると以下のような結果が得られます。

=== RUN   TestAdd
    prog_test.go:21: got 7, want 8
--- FAIL: TestAdd (0.00s)
FAIL

テストコードの可読性は高い一方でテスト結果はシンプルで情報量の足りないものになってしまっています。

ここに subTests によるテストの階層化を行ったコードが以下です。

func TestAdd(t *testing.T) {
    tests := map[string]struct {
        a, b, want int
    }{
        "0+1=1": {0, 1, 1},
        "1+2=3": {1, 2, 3},
        "2+3=5": {2, 3, 5},
        "3+4=8": {3, 4, 8}, // 失敗するテストケース
    }
    for name, tt := range tests {
        t.Run(name, func(t *testing.T) {
            got := Add(tt.a, tt.b)
            if got != tt.want {
                t.Errorf("got %d, want %d", got, tt.want)
            }
        })
    }
}

go.dev

t.Run 関数にテスト名とテスト内容を記述した無名関数を渡すように実装が変更されています。 また、t.Run にはテスト名を渡す必要があるためテストケースを slice ではなく map で持つよう変更しています。 map で持つことでテストケースの実行順序を不規則にするという効果もあります。

=== RUN   TestAdd/1+2=3
=== RUN   TestAdd/2+3=5
=== RUN   TestAdd/3+4=8
    prog_test.go:22: got 7, want 8
=== RUN   TestAdd/0+1=1
--- FAIL: TestAdd (0.00s)
    --- PASS: TestAdd/1+2=3 (0.00s)
    --- PASS: TestAdd/2+3=5 (0.00s)
    --- FAIL: TestAdd/3+4=8 (0.00s)
    --- PASS: TestAdd/0+1=1 (0.00s)
FAIL

テストの実行結果を比較すると subTests なしと比べて各テストの詳細が確認できるようなりました。 どのテストケースが成功して失敗しているのかがテストを階層化することで一目瞭然になりました。

また、subTests にすることで特定のテストケースのみを再実行できるようになります。上記に例で失敗している 3+4=8 の再実行には以下のようにテスト名とテストケース名を指定します。テスト名とテストケース名どちらも正規表現が利用できるので柔軟な指定が可能になっています。

$ go test -run=TestAdd/"3+4=8"

このように1つの関数内に書いた複数のテストケースを個別のテストとして実行するのが subTests の基本的な用途になります。

subTests の並列化

subTests は各テストケースが個別のテストとして実行されるため並列化も簡単に行うことができます。これによりテストの実行時間を短縮したり並列実行時の挙動を確認できます。

並列化を導入するには t.Parallel() を t.Run 内に追加するのみですが、並列実行の場合は tt := tt のように for 内部でイテレータをコピーする必要もあるのに注意が必要です。これは go のイテレータがループ全体で共有されてしまうことへの対処です。

func TestAdd(t *testing.T) {
    tests := map[string]struct {
        a, b, want int
    }{
        "0+1=1": {0, 1, 1},
        "1+2=3": {1, 2, 3},
        "2+3=5": {2, 3, 5},
        "3+4=8": {3, 4, 8}, // 失敗するテストケース
    }
    for name, tt := range tests {
        t.Run(name, func(t *testing.T) {
            tt := tt // イテレータをコピー
            t.Parallel()
            if got := Add(tt.a, tt.b); got != tt.want {
                t.Errorf("got %d, want %d", got, tt.want)
            }
        })
    }
}

go.dev

実行時の並列数は go test -p 4 のように -p フラグで指定します。

subBenchmark の使い方

subTests の Benchmark テスト版が subBenchmarks です。 Benchmark テストも subBenchmarks を利用することで TableDrivenTest に則って書くことができます。Benchmark テストの TableDrivenTest に関する記事もあまり見かけませんが複数のテストケースをまとめられ比較しやすい形で結果を出力できるのでおすすめの書き方です。

こちらの使い方も subTests と同様で b.Run 関数にテスト名とテスト関数を渡します。

func BenchmarkAdd(b *testing.B) {
    tests := map[string]struct {
        a, b int
    }{
        "0+1": {0, 1},
        "1+2": {1, 2},
        "2+3": {2, 3},
        "3+4": {3, 4},
    }
    for name, tt := range tests {
        b.Run(name, func(t *testing.B) {
            Add(tt.a, tt.b)
        })
    }
}

実行すると以下のようにテストケース毎にベンチマークテストの結果が確認できます。 (Benchmark テストは Go Playground では実行できないためコマンドラインから go test -bench . のように実行する必要があります。)

BenchmarkAdd/1+2=3-8            1000000000               0.548 ns/op
BenchmarkAdd/2+3=5-8            1000000000               0.548 ns/op
BenchmarkAdd/3+4=7-8            1000000000               0.548 ns/op
BenchmarkAdd/0+1=1-8            1000000000               0.548 ns/op
PASS

機械学習エンジニア絶賛採用中

マイクロアドでは、問題設定からサーベイ、開発・運用まで裁量を持ってチャレンジしたいという仲間を募集しています!また、機械学習エンジニアだけでなく、サーバサイド、フロントエンド、インフラエンジニアなど幅広く募集しています! 気になった方は以下からご応募ください!

recruit.microad.co.jp