Test
TL;DR
- assertを使うよりも丁寧なレポートメッセージを書くことを意識しよう
- BenchmarkやFuzzingを適切に使うことでソフトウェアの品質をあげよう
- mockを上手に使い環境に依存しないテストをかこう
1. 基本
- Goのテストはgo testでUTやBenchmarkなどさまざまなテストを実行できる
- ファイルのサフィックスに_testがついているものをテスト対象とする
- テストファイルはビルド対象にならない
- 実行例:
go test tdd_test.go main.go -v
1.1 テストの種類
Test:
- プレフィックスにTestがついている
- ユニットテストなど
Benchmark
- プレフィックスにBenchmarkがついている
- パフォーマンスの比較
Fuzz
- プレフィックスにFuzzがついている
- 初期で与えられた値からランダムな値を生成して入力値からバグを発見する
Example
- プレフィックスにExampleがついている
- 出力結果のテストやGoDocにサンプルとして載せることができる
1.2 assert
- Goのtesting packageにはLog、Skip、ヘルパーなどしか存在しない。assertは標準で搭載されていない(3rdパッケージには一応ある)。
- Assertは便利だがエラーメッセージが自動で出力されるため、レポート内容が適当になる
- エラーレポートは何が起きたかを書くことが重要なためしっかりと自分で書く
- 覚えることを増やすことより丁寧に書き、今後のデバッグの際に役立つようにしよう
1.3 Table Driven Test
- GoではテストをTable Driven Testで書くことが推奨されている
- 公式wikiにも書き方などが存在する
- テストケースの入出力を1つのstructにまとめ、すべてのテストケースをforを用いて実行する
ポイント:
- 新しいテストケースの追加が容易
- テーブルをみることで入出力から期待する内容を理解できる
- structとmapでの実行順序が変わる可能性があるため注意する
- Goに限られたテスト手法ではない
tests := []struct {
name string
args args
want int
wantErr bool
}{
// 下が1つのテストケース
{
name: “success”,
args: args {
i: 1,
j: 2,
},
want: 3,
wantErr: false,
},
}
1.4 testing.Parallel
- Goにはtesting.Parallelという関数がある
- testing.Parallelを使うことで同一パッケージ内のテストを逐次ではなく並行に実行できる
注意点:
- サブテストをParallelで並行にする際は値をコピーする(goroutineの起動よりもforを使う方が早い)
- Parallelなサブテストはトップレベルのテストが実行されたあとに処理される
- Table Driven testでカバーできることが多いのでサブテストの採用は慎重に(1Test1Assertion)
(参考:https://qiita.com/marnie_ms4/items/d5233045a084cebeea14)
1.5 raceの検出
- -raceオプションを有効にすることでデータの競合を検出することができる
- 実行例:
go test -race race_test.go main.go -v
- raceするテストではracememを生成したgoroutineとTestRaceのgoroutineどちらからも操作されている
- データ競合を避けるためには排他制御をしてあげる必要がある
1.6 便利メソッド
- Cleanup
- Cleanupを呼び出しているテスト関数は一番最後に呼ばれる
- Parallelを使うとトップレベルが先に終了するため絶対に最後にしたい処理はCleanupを使う
- Setenv
- Cleanupを用いてそのテスト関数だけで使える環境変数をセットできる
- Tempdir
- Cleanupを用いてそのテスト関数だけで使える一時ディレクトリを作成することができる
- Helper
- ヘルパー関数などを読んだときに呼び出し先の行数でエラーなどが出力されるようになる
- テストのレポート情報をより正確に伝えることができる
1.7 Benchmark
- 「推測するな、計測せよ」
- AとBの書き方のどちらを採用するとプログラムのパフォーマンスがいいか悩む
- 採用する際には誰がみてもわかる説得力が重要
- Benchmark Testを書いて判断に理由を持たせる
- 実行例:
go test -bench . -benchmem
ベンチマークの見方
- 数字: 実行した回数
- ns/op: 1回あたりの実行にかかった時間
- B/op: 1回あたりのアロケーションで確保した容量
- allocas/op: 1回あたりのアロケーション回数
1.8 Fuzzing test
- Fuzzingとはプログラムへの入力を継続的に操作して、バグや脆弱性を見つけるソフトウェアテスト手法の一つ
- セミランダムな入力を与えることで、予期しないエッジケースの不具合を見つけるのに有効な手段
- goのtestingパッケージにも搭載された(v1.18 ~)
- (詳しい話:https://qiita.com/s9i/items/de45b820aaeb6597c9a2)
- Go本体ではjsonなどのエンコード、デコードのテストで採用されている
- 実行例:
go test -fuzz FuzzCalc fuzzing_test.go -fuzztime 10s
ポイント:
- Fuzzing testのfuzztimeを決めないと無限に実行される
- CIに組み込む際は絶対にfuzztimeを設定する
- Fuzzing testに失敗した場合testdataに失敗した時の入力値が保存される
- 次の実行はこの入力値から実行されるため修正すればPASSされる
コード:
func FuzzCalc(f *testing.F) {
f.Add(1, 2, "+")
f.Fuzz(func(t *testing.T, v1, v2 int, ope string) { // テストデータの形式を決めている
_, _ = Calc(v1, v2, ope)
})
}
// テストする関数
func Calc(v1, v2 int, ope string) (int, error) {
switch ope {
case "+":
return v1 + v2, nil
case "-":
return v1 - v2, nil
case "*":
return v1 * v2, nil
case "/":
return v1 / v2, nil
}
return 0, errors.New("")
}
1.9 TestとMock
- テストは難しい
- 外部APIやクラウドサービスをどのようにテストするか
- Rate limitを気にしないといけない
- API Keyが必要な場合どのように共有する?
- BQ(Big Query)のようにプロセッシングにお金が絡むようなケース
- -> mockを使う
- 関数の呼び出しを期待する引数で呼ばれることを確認し、ダミーの結果を返却する
- GetUserという関数をUserIDが1である引数で呼ばれることを期待し、成功した場合Name: Gopherを返却する
- mockした関数が期待する引数で呼ばれなかったり、そもそも呼ばれない場合失敗する
- Spyは入出力を記録するだけでMockの呼び出し時点では評価されない ○ Stubは常に何かしら置き換えたものを返すだけ
gomock:
- 定義されたinterfaceからmockを自動生成してくれる
- mockgenコマンドで生成できる
- go generateと一緒につかわれることが多い
- go generateを実行することでファイルに存在する//go:generate ディレクティブを実行できる
mockgenについて:
- 2023年6月23日をもって
github.com/golang/mock
リポジトリが読み取り専用になった - 新しいリポジトリ
go.uber.org/mock
から持ってくる必要がある - (参考:https://zenn.dev/135yshr/articles/6fa5ccc644ba29)