びくんびくんしながらコードを書く。

いしきひくい系エンジニアのらくがき帳

Goのテストコードを書く上でのちょっとしたポイント

この記事は ディップ Advent Calendar 2019ディップ with 全部俺 Advent Calendar 2019の1日目の記事です。

今回はGoでテストを書くときのちょっとしたtipsのご紹介です。

当記事で紹介しているコードは下記githubRepositoryで公開しています。

github.com

TableDrivenテスト

GoではTableDrivenなテストを推奨しています。

TableDrivenとは超絶簡単に表現するとIN/OUTなんかのテストケースを配列で表現し、ループでテスト回そうぜ!みたいな感じです。

実際にはstructのsliceを利用し、Tableを表現します。

実際に実装されているコード

func Sum(a, b int) (s int){
    s = a + b
    return
}

上記に対するテストパターン

    tests := []struct{
        p int
        want string
    }{
        {1,"1"},
        {3,"Fizz"},
        {5,"Buzz"},
        {15,"FizzBuzz"},
    }

これで検証する部分はループを使えます。

   for _, tt := range tests {
        if got := getFizzBuzz(tt.p); got != tt.want{
            t.Errorf("want = %v, got = %v", tt.want, got)
        }
    }

テストケースの名前付け

上記で提示したテストの場合、テストがfailした場合、どのケースでテストが失敗したか分かりづらい状態です。

少数のテストケースの場合、今の状態でもあまり大きな問題にはなりませんが、テストケースが多い場合、辛くなることが予想されます。

テストケースの名前付けは今の実装のまま、structに name string を付与し、下記の実装のようにエラー時に名前を出力する方法もありますが、次のsectionで説明するサブテスト化を行ったほうが有効です。

   tests := []struct{
        name string
        p int
        want string
    }{
        {"Number",1,"1"},
        {"Fizz",3,"Fizz"},
        {"Buzz",5,"Buzz"},
        {"FizzBuzz",15,"FizzBuzz"},
    }

これでtestがfailのときにテストケースが出力されます。

   for _, tt := range tests {
        if got := Sum(tt.a, tt.b); got != tt.want{
            t.Errorf("TestCase: %v want = %v, got = %v", tt.want, got)
        }

    }

SubTest化する

テストケース名をエラー出力に出す方式ではなく、SubTest化してテストを実施する方法です。

サブテスト化する前までのテスト結果は下記のような出力となります。

func TestSum(t *testing.T) {
    tests := []struct{
        name string
        a, b, want int
    }{
        {"Simple", 0, 1, 1},
        {"Minus", -1 , -1, -2},
        {"Both", -3, 2, -1},
    }

    for _, tt := range tests {
        if got := Sum(tt.a, tt.b); got != tt.want{
            t.Errorf("TestCase: %v want = %d, got = %d", tt.name, tt.want, got)
        }
    }
}
$ go test . -v
=== RUN   TestGetFizzBuzz
--- PASS: TestGetFizzBuzz (2.01s)

サブテスト化するメリットは - それぞれのテストケースでOK,NGを判定してくれる - 特定のサブテストのみ実行できる - テストの並列実行ができる などがあります。

サブテスト化したコードは下記のようになります。

func TestGetFizzBuzz(t *testing.T) {
    tests := []struct{
        name string
        p int
        want string
    }{
        {"Number",1,"1"},
        {"Fizz",3,"Fizz"},
        {"Buzz",5,"Buzz"},
        {"FizzBuzz",15,"FizzBuzz"},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T){
            if got := getFizzBuzz(tt.p); got != tt.want{
                t.Errorf("Want = %v, got = %v", tt.want, got)
            }
        })
    }
}

testing.T.Runを使いサブテスト化します。
そのさい、第一引数にテストケース名を指定します。
これを実行すると、下記のような実行結果になります。

$ go test . -v
=== RUN   TestGetFizzBuzz
=== RUN   TestGetFizzBuzz/Number
=== RUN   TestGetFizzBuzz/Fizz
=== RUN   TestGetFizzBuzz/Buzz
=== RUN   TestGetFizzBuzz/FizzBuzz
--- PASS: TestGetFizzBuzz (2.01s)
    --- PASS: TestGetFizzBuzz/Number (0.50s)
    --- PASS: TestGetFizzBuzz/Fizz (0.50s)
    --- PASS: TestGetFizzBuzz/Buzz (0.50s)
    --- PASS: TestGetFizzBuzz/FizzBuzz (0.50s)

各テストケースがパスしたかどうかがかなり見やすくなったかと思います。

また、特定のサブテストのみを実行する場合は下記の通りです。

$ go test . -v -run GetFizzBuzz/Number
=== RUN   TestGetFizzBuzz
=== RUN   TestGetFizzBuzz/Number
--- PASS: TestGetFizzBuzz (0.50s)
    --- PASS: TestGetFizzBuzz/Number (0.50s)
PASS
ok      github.com/bikun-bikun/go-test  0.513s

このようにサブテスト化するメリットは多いと思います。 並列テストにつてはボリュームが多くなりそうなので明日、別の記事にて記載したいと思います。

Goのテストに関して、下記記事が非常によくまとまっているためより詳細に知りたい場合は下記を確認することをおすすめします。

budougumi0617.github.io