質問と想定されるシナリオ
本番環境で Unicode、切り捨てられたバイト列、または極端な長さを時折処理する Go の入力パーサーを保守しています。testing.F ファズターゲットを設計し、シードを選択し、正確な出力が未知である場合にプロパティを表現し、修正後に失敗例をリグレッションとして保持する方法を説明してください。
これはバックエンド、インフラストラクチャ、およびテストツール開発の役割に適しています。Microsoft の技術面接ガイダンスではテスト、境界、セキュリティへの影響を明示的に評価しており、Amazon の SDE ガイダンスではプログラミングとコアソフトウェアのトピックを挙げています。面接での評価シグナルはコマンドの暗記ではなく、エンジニアリングループです。
面接官がテストしていること
- サンプルテストの期待値とファズテストのプロパティの区別。
- 外部の副作用を持たない高速なターゲットの選択。
- シードコーパス、カバレッジ誘導、最小化、およびリプレイの説明。
- 不正な UTF-8、空の入力、過大な入力、およびリソース制限の処理。
- 発見、診断、修正、再実行、およびコミットされたコーパスの連携。
不十分な回答では「ランダムな入力を生成する」と答えます。優れた回答では、不変条件、コマンド、失敗パス、および CI の分離を明確に示します。
回答前の確認事項
- パーサーの入力は
string、[]byte、それとも複数のフィールドですか?これによりファズ引数とコーパスのエンコーディングが決まります。 - どの無効な入力が期待されるエラーですか?エラーコントラクトによってアサーションが変わります。
- 呼び出しあたりのリソースバジェットはどのくらいですか?処理の遅い入力はサービス拒否の兆候である可能性があります。
- パニック、セマンティックバグ、互換性のリグレッションのどれを探していますか?それぞれ異なるプロパティとシードが必要です。
30秒の回答フレームワーク
「パース後にエンコードしても同じ構造が維持される」または「無効な入力は制御されたエラーを返す」といった検証可能なプロパティから始めます。f.Add で実際の境界ケースを追加し、f.Fuzz を高速、決定論的、かつ制限された状態に保ちます。通常の go test でシードと保存された失敗例を実行し、独立した CI ジョブで時間制限を設けて go test -fuzz を実行します。Go は失敗した入力を testdata/fuzz/<name> に最小化します。根本原因を修正した後、その入力をリプレイし、テストスイート全体を実行し、コーパスをコミットすることで、発見された問題がリグレッションの制約となります。
ステップバイステップの詳細な回答
1. 期待される答えではなく、プロパティから始める
パーサーの場合、ラウンドトリッププロパティを使用します。正規化後、Encode(Parse(x)) は x を表す必要があります。文字列変換の場合は、冪等性、長さの保持、または UTF-8 の有効性を使用します。指定された正規化を許容し、バイトレイアウトをコントラクトと混同しないでください。
2. 制限を設けたファズターゲットを構築する
func FuzzParseRoundTrip(f *testing.F) {
f.Add([]byte("name=alice"))
f.Add([]byte{})
f.Fuzz(func(t *testing.T, input []byte) {
t.Helper()
if len(input) > 1<<20 {
t.Skip()
}
got, err := Parse(input)
if err != nil {
return
}
again, err := Parse(Encode(got))
if err != nil || !Equal(got, again) {
t.Fatalf("round trip failed: %v", err)
}
})
}f.Add の型と順序はコールバックと一致している必要があります。ターゲットはファイルの書き込み、ネットワークの呼び出し、または時間への依存を行うべきではありません。そうしないと、並行ファジングによって再現性のない失敗が発生します。
3. ビジネス境界をシードする
シードは、プロトコルのバージョン、空の値、重複したフィールド、非 ASCII テキスト、切り捨てられたデータ、およびサニタイズされた本番サンプルを網羅する必要があります。Go は通常のテスト中にシードを実行するため、各シードは軽量で安定していなければなりません。
4. 実行モードを分離する
コミット前に go test -run=FuzzParseRoundTrip を実行してシードを確認します。専用のジョブで go test -fuzz=FuzzParseRoundTrip -fuzztime=10s を実行できます。時間フラグによって探索範囲が制限されます。並行処理とパッケージのバジェットは本番コードではなく CI が管理します。
5. 最小化された入力をリプレイして診断する
Go は依然として失敗を引き起こす入力を最小化し、testdata/fuzz/FuzzParseRoundTrip/ の下に書き込みます。go test -run=FuzzParseRoundTrip/<id> でリプレイし、根本原因(アサーション、パニック、リソース枯渇、またはテストの非決定性)を分類します。サンプルは不透明なバイナリデータではなく、バグを説明するものであるべきです。
6. 修正を恒久化する
修正後に失敗例をリプレイし、すべての go test を実行します。保存された失敗例は -fuzz なしでも実行されるため、コミットする前にコーパスに機密情報が含まれていないか、墨消し(リダクション)されているか、サイズ制限を超えていないかを確認してください。
7. ファジングの品質を測定する
カバレッジの増加が止まった場合は、実行時間を増やすだけでなく、構造化されたシードを追加するか生成方法を改善します。タイムアウトによる失敗が頻発する場合は、入力を小さくするか、負荷の高いパスを分離する必要があります。これらはパフォーマンスの欠陥として扱います。発見率の低下は正しさの証明にはなりません。
高品質な回答例
私はファジングを、すべてのランダム入力に対する期待されるビジネス結果ではなく、再現可能なプロパティを中心に定義します。パーサーの場合、有効な入力はパースされて同じ構造に再エンコードされるべきであり、無効な入力は制御されたエラーを返し、決してパニックを起こしてはなりません。空の入力、受け入れ可能な最大サイズ、重複フィールド、Unicode、サニタイズされた本番ケースをシードします。ターゲットは制限されており、副作用はありません。開発者は go test -run=FuzzX を実行し、CI は固定された -fuzztime で探索します。Go が最小化された失敗を書き込んだら、それをリプレイし、根本原因を特定し、実装を修正し、通常のテストとファジングを実行して、testdata/fuzz/FuzzX をコミットします。これにより、1つの発見がすべての変更に対するリグレッションチェックになります。
よくある間違い
- ファジングをランダムな負荷として扱う → オラクルが存在しない → まず不変条件とエラーコントラクトを定義する。
- ターゲットからライブサービスを呼び出す → ネットワークと状態により失敗がフレイキーになる → 決定論的なフェイクを使用する。
- 保存された失敗例をファズモードでのみ実行する → 修正がリグレッションする可能性がある →
testdata/fuzzに保持する。 - 制限のない入力を受け入れる → 1つのケースがバジェットを消費する → 制限を適用し、スキップを記録する。
- 生成されたすべてのケースをコミットする → リポジトリのノイズと肥大化 → バグを再現するケースや主要な分岐をカバーするケースのみを保持する。
フォローアップの質問と回答
パースによって入力が正規化される場合、ラウンドトリップアサーションは厳しすぎますか?
はい。正規化された AST またはフィールドセットを比較し、意図的に無視される順序、空白、または大文字小文字の違いを明示してください。
ユーザーの機密情報を含む失敗サンプルをコミットできますか?
いいえ。クレデンシャルや個人データを確認し、墨消しを行って、墨消しされたサンプルでも依然として失敗することを確認します。安全にコミットできない場合は、制御された内部での再現性を維持し、合成された同等のデータを提出します。
CI のバジェットが5分です。ファジングをどのように適合させるべきですか?
ビルドごとにシードと既知の失敗例を実行します。時間、パッケージ、リソースによって探索を制限し、サイレントにスキップするのではなくタイムアウトを記録します。
ファズテスト自体が壊れているかどうかをどのように見分けますか?
最小化されたケースをリプレイし、グローバル状態、ランダム性、ゴルーチンのタイミングを検査します。次に決定論的な単体テストを作成します。それがフレイキーである場合は、本番コードの前に分離を修正します。
複数のターゲットでコーパスを共有できますか?
入力形式とセマンティクスが一致している場合にのみ変換ロジックを共有します。あるターゲットのコーパスが別のターゲットのカバレッジギャップを隠してしまわないよう、ディレクトリと不変条件は分離してください。