Goにおけるテストとベンチマークの統合ツールキット:testing.TBインターフェースの活用

Goの標準テストパッケージは、単体テスト用のtesting.Tとベンチマークテスト用のtesting.Bという2つの主要な型を提供しています。興味深いことに、これらの型はどちらもtesting.TBインターフェースを実装しており、これにより、ログ出力、失敗処理、テストのスキップ、並列実行、リソース管理といった共通の機能セットを共有しています。testing.Bはさらに、パフォーマンス測定に特化した独自のメソッド群を備えています。

以下では、testing.TBインターフェースによって共有されるメソッドと、testing.B固有のメソッドについて、その機能と目的を整理します。

1. testing.T と testing.B が共有するメソッド (testing.TBインターフェース)

これらのメソッドは、単体テストとベンチマークテストの両方で直接利用可能です。

1.1. ログ記録

  • Log(args ...any) / Logf(format string, args ...any): テストの標準出力へ情報を書き込みます。デフォルトでは、テストが失敗した場合、または -v フラグ付きで実行された場合にのみ表示されます。

1.2. 失敗処理

  • Fail(): 現在のテストを失敗とマークしますが、ログは記録せず、実行も中断しません。後続のコードは実行され続けます。
  • Failed() bool: テストが既に失敗しているかどうかを返します。
  • Error(args ...any) / Errorf(format string, args ...any): Log の後に Fail を呼び出すのと同等です。エラー情報を記録し、テストを失敗とマークしますが、実行は継続します。
  • Fatal(args ...any) / Fatalf(format string, args ...any): Log の後に FailNow を呼び出すのと同等です。エラー情報を記録し、現在のテストを直ちに終了させます (runtime.Goexit を呼び出します)。前提条件が満たされない場合にテストを中止する際によく使用されます。

1.3. テストのスキップ

  • Skip(args ...any) / Skipf(format string, args ...any): スキップ理由を記録し、テストを SKIP とマークして直ちに終了させます。
  • SkipNow(): テストを直ちに終了させますが、理由の記録は行いません。条件判断と組み合わせて使用されることが多いです。
  • Skipped() bool: テストがスキップされたかどうかを返します。

1.4. 並行実行

  • Parallel(): このテストが、同様に Parallel を呼び出す他のテストと並行して実行可能であることを宣言します。これにより、全体の実行時間を短縮できます。通常、テーブル駆動テストの重いケースで利用されます。

1.5. サブテスト / サブベンチマーク

  • Run(name string, f func(t *testing.T)) (T 上): 指定された名前でサブテストを作成し実行します。
  • Run(name string, f func(b *testing.B)) (B 上): 指定された名前でサブベンチマークを作成し実行します。サブテスト/サブベンチマークは親の Cleanup や TempDir を共有でき、テストレポートはツリー構造で出力されます。テーブル駆動テストや階層的なテスト構成に最適です。

1.6. ヘルパー関数のマーク付け

  • Helper(): 現在の関数をテストヘルパー関数としてマークします。これにより、ヘルパー関数内で Error や Fatal を呼び出した場合、失敗の引用元ファイル名と行番号は、ヘルパー関数内部ではなく、呼び出し元を指すようになります。問題の特定が容易になります。

1.7. リソースのクリーンアップ

  • Cleanup(f func()): クリーンアップ関数を登録します。この関数は、テスト(およびそのすべてのサブテスト)が完全に終了した後に、後入れ先出し (LIFO) の順序で実行されます。テストでパニックが発生した場合でも、これらのクリーンアップ関数は呼び出されます。defer に似ていますが、より広範なスコープを持ち、t.Run 間でのリソース共有にも安全に使用できます。

1.8. 一時ディレクトリ

  • TempDir() string: 自動的に作成された一時ディレクトリのパスを返します。テスト終了時に、このディレクトリとその中のすべての内容は自動的に削除されます。os.MkdirTemp と defer os.RemoveAll を手動で組み合わせる手間を省きます。

1.9. 環境変数の分離

  • Setenv(key, value string): 現在のテストのために環境変数を設定し、テスト終了時に元の値に復元するクリーンアップ関数を登録します。並行テスト (t.Parallel()) 中に呼び出すとパニックが発生します。これにより、環境変数の相互上書きによる競合状態を防ぎます。 (Go 1.17 で導入)

1.10. タイムアウト検知とコンテキスト

  • Deadline() (deadline time.Time, ok bool): -timeout フラグで設定されたテストの締め切り時間を返します。ok が true の場合、コード内で残り時間を判断し、操作のタイムアウトを防ぐことができます。 (Go 1.15 で導入)
  • Context() context.Context: テスト終了時に自動的にキャンセルされる context.Context を返します(タイムアウト、Cleanup 実行前など)。context.Context を受け入れる被テスト関数に渡したり、バックグラウンドゴルーチンのライフサイクルを制御したりするのに最適です。 (Go 1.24 で導入)

1.11. テスト名

  • Name() string: 現在のテスト(またはサブテスト)の完全な名前を返します。例: TestFoo/subtest_bar。

2. testing.B 固有のメソッド (ベンチマークテスト専用)

これらのメソッドは、ベンチマークテストのタイミングとレポートを正確に制御するために使用されます。

2.1. コアループの属性

  • N int: ベンチマークフレームワークが自動的にこの値を調整し、測定対象のコードが安定した時間測定を取得するのに十分な回数実行されるようにします。ベンチマーク関数は、for i := 0; i < b.N; i++ の構造を使用する必要があります。

2.2. タイマー制御

  • StartTimer(): ベンチマークテストのタイマーを開始または再開します。StopTimer と組み合わせることで、測定対象のコードセグメントを正確に制御できます。
  • StopTimer(): ベンチマークテストのタイマーを一時停止します。現在タイマーが実行中でない場合は、副作用はありません。通常、測定に含めたくない初期化やクリーンアップ操作を除外するために使用されます。
  • ResetTimer(): 経過時間をゼロにし、イテレーションカウントをリセットします (b.N には影響しません)。典型的な使用法は、for ループの前に呼び出し、b.N ループ外の準備コストを除外することです。

使用例:

func BenchmarkSomething(b *testing.B) {
    data := expensiveSetup()
    b.ResetTimer() // セットアップの所要時間を無視
    for i := 0; i < b.N; i++ {
        process(data)
    }
}

2.3. スループットレポート

  • SetBytes(n int64): 1回の操作で処理されるバイト数(通常は入力/出力データサイズ)を設定します。ベンチマーク出力に MB/s や B/op などのスループット情報が自動的に計算され、表示されます。

2.4. カスタムメトリックレポート

  • ReportMetric(n float64, unit string): ベンチマーク結果にカスタムパフォーマンスメトリックを追加します。例:
    • "alloced_bytes_per_op": 1操作あたりの割り当てバイト数(testing.AllocsPerRun などと併用)
    • "custom_mops_per_sec": 任意のカスタム単位
    同じ単位で複数回呼び出すと累積され、異なる単位は個別に表示されます。通常、ResetTimer の後に呼び出し、測定フェーズのメトリックのみを報告します。

2.5. ベンチマーク時間取得

  • Elapsed() time.Duration: ベンチマークテストのタイマーが記録した累積時間(つまり、測定された総時間であり、StopTimer で一時停止された期間は含まない)を返します。ベンチマーク関数の末尾でカスタム計算を行う際に、直接 time.Now を使用するよりも実際の測定時間をより正確に反映します。(Go 1.20 で導入)

2.6. 並列ベンチマーク

  • RunParallel(body func(pb *testing.PB)): 複数のゴルーチンを使用して被テスト関数を並列実行し、同時実行負荷をシミュレートします。各ゴルーチンは pb.Next() を通じて次のイテレーションを取得します。
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            doWork()
        }
    })
    
    testing.PB は Next() bool メソッドのみを提供し、並行数は GOMAXPROCS または -cpu フラグによって制御されます。

2.7. サブベンチマーク

  • Run(name string, f func(b *testing.B)): t.Run と同じ動作ですが、ベンチマークテスト用です。これにより、同じベンチマーク関数内で複数のシナリオを整理し、セットアップ/ティアダウンコードを共有できます。

3. まとめ

  • 共通メソッド (testing.TB 経由): logging, failure/skip, parallel execution, subtests, helper marking, resource cleanup, temporary directories, environment isolation, timeouts and contexts を制御し、堅牢で保守性の高いテストを構築できます。
  • ベンチマーク専用メソッド (testing.B 固有): precise timer control, throughput reporting, custom metric reporting, parallel execution models を中心に、パフォーマンス分析と回帰検出に必要なツールを提供します。

これらのメソッドを理解することで、よりクリーンで表現力豊かなテストコードとベンチマークコードを作成できます。

タグ: Go testing benchmark testing.TB unit test

10月18日 10:23 投稿