ファジングテスト、コンコリックテストなど、テスト入力を自動生成・大量使用するテストで問題になるのが、テストオラクル(テストの期待値を提供するもの)をどう実装するかです。このテストオラクル問題への対策の代表例については、次のようなアプローチがあります。
- 期待値を生成できるテストオラクルを別に用意する(モデル駆動開発、実行可能な仕様など)
- テスト対象から期待値を予測できる条件でテストする(メタモルフィックテスティングなど)
- テスト入力後、動作不能でないこと・エラーが記録されていないことを大まかに確認する(ストレステストなど)
- テスト結果の変化の監視にフォーカスする(ビジュアルリグレッションテストなど)
このうち「テスト対象から期待値を予測できる条件でテストする」の実装例の一つにラウンドトリップテスト(Roundtrip Test)があります。
※注意事項として、ラウンドトリップテストは複数の意味で用いられる用語です。例えばxUnit Test PatternsではEnd to Endの外部インターフェース経由でSUTとやり取りするテストをラウンドトリップテストと呼称しています。現状では、どの定義で使われているかは文脈で判断する必要があります。
ラウンドトリップテストとは
ラウンドトリップテストとは、入力データに処理をかけ、その結果に逆処理をかけた結果が、元の入力データと一致するか確認するテストを指します。
具体例として以下があります。
- エンコードした後、デコードして、元のデータに戻ることをテスト(例えば暗号化処理のテスト)
- 圧縮した後、解凍して、元のデータに戻ることをテスト(例えば可逆画像圧縮のテスト)
- 出力し、それを読み取って、出力データと一致することをテスト(例えばデータ記録のテスト)
ラウンドトリップテストでは、入力データと実行結果を比較するのみのため、別にテストオラクルを実装する必要がなくなります。また「システムに異常が発生しない」「ログにエラーが記録されない」のような大雑把な確認だけでなく、ある程度詳細に動作を確認できます。
そのため、ファジングやストレステストのような、テスト入力を大量に自動生成するアプローチで、テストオラクル問題の改善に寄与します。
ファジングでのラウンドトリップテストの実装例として以下があります。
実装例
前述のGIFのラウンドトリップテストのコードを転載します。GIF変換処理について、エンコードし、デコードしたデータが元に戻ることを大まかに確認しています。この確認を、Go Fuzzのファジングライブラリで自動生成したテスト条件で実行することで、ラウンドトリップテストを実現しています。
f.Fuzz(func(t *testing.T, b []byte) { cfg, _, err := image.DecodeConfig(bytes.NewReader(b)) if err != nil { return } if cfg.Width*cfg.Height > 1e6 { return } img, typ, err := image.Decode(bytes.NewReader(b)) if err != nil || typ != "gif" { return } for q := 1; q <= 256; q++ { var w bytes.Buffer err := Encode(&w, img, &Options{NumColors: q}) if err != nil { t.Fatalf("failed to encode valid image: %s", err) } img1, err := Decode(&w) if err != nil { t.Fatalf("failed to decode roundtripped image: %s", err) } got := img1.Bounds() want := img.Bounds() if !got.Eq(want) { t.Fatalf("roundtripped image bounds have changed, got: %v, want: %v", got, want) } } })
ラウンドトリップテストを使うときの戦略立て
ラウンドトリップテストは、「処理をかけ、逆処理をかけたら元に戻る関係」のみを確認するため、それだけではテストはまったく不十分です。用意した期待値と比較する通常のテストの代替にはなりません。
ラウンドトリップテストは、それら通常のテストを補強するための、ファズデータに対する耐性や堅牢性を確認するための追加確認として活用するのが現実的な戦略になります。