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

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

Goのテストコードを並列化する。

この記事は ディップ with 全部俺 Advent Calendar 2019 の二日目の記事です。
一日目に引き続きGoのテストコードについてまとめていきたいと思います。

テストの並列化の前に並列処理を行う上での注意点を見てみましょう。

ループ変数の補足

並列処理を行うに当たり、ループして同じ処理を繰り返す場合に、意図しない挙動になることがあります。
下記に例を提示します。

The Go Playground

実行してみるとわかりますが、自分の意図した挙動と異なるかと思います。
関数実行がループよりも後になり、ループとしてはすでに終了した最後のiが関数内で利用されることでこのようなことが起こります。

これを回避するには下記のように修正します。

The Go Playground

これは、ループの内側でループの外側の i を補足し、並列実行される関数に渡します。 このため、意図した挙動になるのです。

テストの並列化

上記にしたことに注意し、前回記事のサブテスト化からサブテストの並列実行に移りましょう。

並列実行が有効な事がわかるように今回の実装では関数内でsleepを掛けます。

実装部

func getFizzBuzz(p int) (r string){
    if p % 3 == 0 {
        r = r + "Fizz"
    }

    if p % 5 == 0 {
        r = r + "Buzz"
    }

    if r == "" {
        r = strconv.Itoa(p)
    }

    time.Sleep(500 * time.Millisecond)

    return
}

テスト部

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 {
        tt := tt
        t.Run(tt.name, func(t *testing.T){
            t.Parallel()
            if got := getFizzBuzz(tt.p); got != tt.want{
                t.Errorf("Want = %v, got = %v", tt.want, got)
            }
        })
    }

}

サブテスト内で t.Parallel() を呼び出すことで並列化出来ます。
また、ループが始まってすぐの部分でテストケースの補足を行います。

このこの状態で実行すると

$ go test . -v
=== RUN   TestSum
=== RUN   TestSum/Simple
=== RUN   TestSum/Minus
=== RUN   TestSum/Both
--- PASS: TestSum (0.00s)
    --- PASS: TestSum/Simple (0.00s)
    --- PASS: TestSum/Minus (0.00s)
    --- PASS: TestSum/Both (0.00s)
=== RUN   TestGetFizzBuzz
=== RUN   TestGetFizzBuzz/Number
=== PAUSE TestGetFizzBuzz/Number
=== RUN   TestGetFizzBuzz/Fizz
=== PAUSE TestGetFizzBuzz/Fizz
=== RUN   TestGetFizzBuzz/Buzz
=== PAUSE TestGetFizzBuzz/Buzz
=== RUN   TestGetFizzBuzz/FizzBuzz
=== PAUSE TestGetFizzBuzz/FizzBuzz
=== CONT  TestGetFizzBuzz/Number
=== CONT  TestGetFizzBuzz/Buzz
=== CONT  TestGetFizzBuzz/Fizz
=== CONT  TestGetFizzBuzz/FizzBuzz
--- PASS: TestGetFizzBuzz (0.00s)
    --- PASS: TestGetFizzBuzz/Buzz (0.50s)
    --- PASS: TestGetFizzBuzz/Fizz (0.50s)
    --- PASS: TestGetFizzBuzz/Number (0.50s)
    --- PASS: TestGetFizzBuzz/FizzBuzz (0.50s)
PASS
ok      github.com/bikun-bikun/go-test  0.512s

と sleep 分くらいの実行時間でテストが終了します。 それに対して並列処理を外すとどうなるか見てみましょう

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 {
        //tt := tt //コメントアウトで無効にしてみる
        t.Run(tt.name, func(t *testing.T){
            //t.Parallel() //コメントアウトで無効にしてみる
            if got := getFizzBuzz(tt.p); got != tt.want{
                t.Errorf("Want = %v, got = %v", tt.want, got)
            }
        })
    }

}

実際に実行してみると

$ go test . -v
=== RUN   TestSum
=== RUN   TestSum/Simple
=== RUN   TestSum/Minus
=== RUN   TestSum/Both
--- PASS: TestSum (0.00s)
    --- PASS: TestSum/Simple (0.00s)
    --- PASS: TestSum/Minus (0.00s)
    --- PASS: TestSum/Both (0.00s)
=== 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)
PASS
ok      github.com/bikun-bikun/go-test  2.017s

ということで、直列で実行した通りの時間となりました。

CIなどでテストを回す場合や、ローカルでテストを回す場合、テストのボリュームが大きくなるにつれ、テストの時間が増えていきます。

こうした細かい部分で処理時間を短くしていきましょう。