Go1.7のSubtestsとSub-benchmarks

Go1.7ではSubtestsとSub-benchmarksという機能がtestingパッケージに導入される.これを使うとテスト関数/ベンチマーク関数の中にテスト/ベンチマークを定義できるようになる.テストの場合はテストに階層を持たせることができ,ベンチマークの場合はTable Driven的にベンチマークを記述することができるようになる.さらに一連のテスト/ベンチマークに対して共通のsetupとtear-downを持たせることもできる.

テストの場合はTable Driven Testsで十分なことも多く恩恵は少ないかもしれない.それよりもベンチーマークで効果を発揮することが多い.

例えば以下のように異なる設定値を使ってFooのベンチマークをとるとする.今までであればそれぞれ設定値ごとにベンチマーク関数を準備する必要があった.

func BenchmarkFoo1(b *testing.B)   { benchFoo(b, 1) }
func BenchmarkFoo10(b *testing.B)  { benchFoo(b, 10) }
func BenchmarkFoo100(b *testing.B) { benchFoo(b, 100) }

func benchFoo(b *testing.B, base int) {
    for i := 0; i < b.N; i++ {
        Foo(base)
    }
}

Go1.7のSub-benchmarkを使うと以下のように書ける.

func BenchmarkFoo(b *testing.B) {
    cases := []struct {
        Base int
    }{
        {Base: 1},
        {Base: 10},
        {Base: 100},
    }

    for _, bc := range cases {
        b.Run(fmt.Sprintf("%d", bc.Base), func(b *testing.B) { benchFoo(b, bc.Base) })
    }
}

まず複数の関数を一つの関数にまとめることができる.そして設定値をTable-Driven的に書くことができる.これによりシンプルになりかつ可読性も上がる.Sub-benchmark名はトップレベルの関数名(BenchmarkFoo)とRun関数に渡す文字列を/で繋いだものになる.例えば上の例の場合はBenchmarkFoo/1…となる.またforループの前後にBenchmarkFoo専用のsetup・tear-down処理を記述することもできる.

標準パッケージの変更を見ていてもSubtestよりもSub-benchmarkで恩恵を得ているのがわかる.例えば以下のような使われ方をしている.

またSubtestsはParallelテストの制御にも使える.t.Parallel()を使えば個々のテストは並行処理される.そして全てのサブテストが終了した時点でトップレベルの関数に戻る.これを使えば,並行でテストを走らせて全ての処理が終了したら後処理を行うといったことができる.

まとめ

GopherCon 2016のLT “State of Go 2016” によるとベンチマークを書いてるひとはまだ少ない.この変更でもう少し増えると良い.