| name | generative-fuzzing |
| description | 期待値(oracle)を用意しにくい対象の頑健性を、機械生成した不正・極端・ランダムな入力を 大量に流し込んで叩く手法(ファジング、カバレッジガイデッドファジング)を扱う。 test-catalog の手法カタログの一部。パーサー・デシリアライザ・入力検証など信頼できない 入力境界で「壊れない・止まらない・不変条件を保つ」ことを検証したい、または割り当てたい ときに使う。通常は test-catalog スキルの索引経由で手法が選定された後にこのスキルを 直接参照する。
|
| disable-model-invocation | true |
頑健性を叩く生成テスト(ファジング / カバレッジガイデッドファジング)
不正・極端・ランダムな入力を大量に流し込み、「壊れないこと」(クラッシュ・ハング・未処理例外・不変条件違反が起きないこと)を叩く系をまとめる。
性質や組合せで正しさを縛る系(PBT / コンビナトリアル)は generative-property.md に分けた。
ファジングは出力の正解を要求しない点が要で、判定は「落ちない・止まらない・不変条件を保つ」で済む。
だからこそ「何をもって十分叩いたか」(到達した分岐・育てたコーパス・打ち切り判定)を決めないと、回しただけで終わる。各項目は「概要」「目的/いつ使う」「TypeScript example」「落とし穴」「遂行手順(着手→完了)」「完了チェック(もれ確認)」「網羅設計での効かせ方」の構成で示す。
目次
ファジング(Fuzzing)
概要
不正、極端、ランダムなバイト列や文字列を大量に流し込み、クラッシュ、ハング、assertion 違反、未処理例外を起こす入力を探す。
目的/いつ使う
パーサー、デシリアライザ、入力検証、ファイル/プロトコル境界など「信頼できない入力」を受ける箇所の頑健性を確かめる。
判定は「壊れないこと」(落ちない、無限ループしない、不変条件を保つ)で済むので、出力の正解が要らないのが利点。
出力の意味的正しさまで問いたいなら、ファジングではなく PBT やメタモルフィックを使う。
TypeScript example
fast-check をファザーとして使い、任意文字列でパーサーが例外を投げず必ず判定を返すことを確認する。
import { describe, it, expect } from "vitest";
import fc from "fast-check";
import { parseConfig } from "./config";
describe("parseConfig: fuzzing", () => {
it("never throws and always returns ok|error for any string", () => {
fc.assert(
fc.property(fc.string(), (raw) => {
const r = parseConfig(raw);
expect(r.kind === "ok" || r.kind === "error").toBe(true);
}),
{ numRuns: 5000, seed: 1 },
);
});
});
落とし穴
- 「落ちない」だけを見ると、黙って誤った値を返すバグはすり抜ける。不変条件チェックを併せて入れる。
- 反例の再現にはシード固定が要る。fast-check は失敗時にシードを出すので控える。
遂行手順(着手→完了)
ファジングの本体は「信頼できない入口を1つ決め、壊れの定義(オラクル)を据え、打ち切りまで叩く」ことだ。次の順で進める。
- 対象の入口を1つに決める:信頼できない入力を最初に受ける関数(
parseConfig・deserialize・decodeFrame など)を1つ選ぶ。境界をまたぐ複数関数を一度に叩かない(どこで壊れたか切り分けられなくなる)。
- 壊れの定義(オラクル)を据える:正解を持たない代わりに「壊れたとみなす条件」を明示する。次を満たさなければ失敗とする。
- クラッシュ/未処理例外を投げない(投げたらテストランナーが失敗扱いにする)。
- ハングしない(無限ループ・指数時間に陥らない。必要ならタイムアウトを掛ける)。
- 不変条件を保つ:
r.kind が ok|error のいずれか、長さ・範囲・型の制約を満たす、など。ここを省くと黙って誤値を返すバグがすり抜ける(落とし穴)。
- 入力源を広く生成する:
fc.string()・fc.uint8Array() など、対象が実際に受けるバイト/文字空間を広く覆う生成器を選ぶ。狭い生成器(英数字のみ等)は異常系を踏まない。
- 初期コーパス(種入力)を投入する:実データ・過去のバグ入力・仕様上の代表例を種として与え、そこから変異させる。種があると素朴な乱数より早く奥の分岐へ届く。fast-check 単体なら既知の失敗例を別テストで固定し、種の役割を持たせる。
- 打ち切り判定を決める:
numRuns(試行回数)か wall-clock 時間で打ち切る。「何回(何分)叩いて壊れなければ緑とみなすか」を明示し、CI で回す回数と夜間で回す回数を分ける。
- 失敗の最小化と回帰固定:落ちたら fast-check が出す最小反例と seed を保存し、例ベースの回帰テストに固定する。コーパス/種を捨てると次回ゼロから探索になる。
完了チェック(もれ確認)
- 入口が1つに絞られているか:1テストが叩く対象が単一の信頼境界関数か。複数を束ねていれば切り分け不能。
- 不変条件チェックが併載されているか:「例外を投げない」だけで終わっていないか。
grep -n "expect" <test> で、戻り値の不変条件(kind が ok|error 等)を assert しているか確認する。これが無いと誤値バグがすり抜ける。
- 生成器が異常系を踏むか:制御文字・非 UTF8 バイト・空・巨大入力を踏む生成器か。狭い生成器なら頑健性を叩けていない。
- 打ち切り基準が明示されているか:
numRuns か時間制限が指定されているか。無ければ「十分叩いた」が無根拠。
- 最小反例が回帰に固定されたか:過去に落ちた最小入力と seed が恒久テストに残っているか。捨てていれば同じクラッシュが再発する。
網羅設計での効かせ方
ファジングの網羅対象は入出力の対応ではなく、「信頼できない入力境界で壊れを起こす入力が存在しないこと」だ。
正解 oracle を持たないので、網羅は「到達した分岐」と「育てたコーパス」で近似する(完全な網羅は測れない、近似であると割り切る)。
壊れの定義(クラッシュ・ハング・不変条件違反)が網羅対象の集合を決める。不変条件を1つ足すごとに、ファジングが検出できる「壊れ」の種類が増える。
ゆえに頑健性の網羅を広げる手は、入力生成器を広げることと、不変条件を増やすことの2方向になる。
出力の意味的正しさ(値が仕様どおりか)はファジングの網羅対象外なので、そこは PBT・メタモルフィック(generative-property.md、oracle-relational.md)へ振る。
配分の原則は、信頼境界の「落ちなさ」はファジングで近似網羅し、意味的正しさは性質ベースに分担させることだ。
カバレッジガイデッドファジング(Coverage-Guided Fuzzing)
概要
実行時のコードカバレッジを計測し、新しい経路を開拓した入力を「種」として残して変異させる。
ランダムな総当たりより遥かに速く深いパスへ到達する(AFL/libFuzzer 系の発想)。
目的/いつ使う
パーサーやバイナリ処理など分岐が深く、素朴なランダム入力では奥まで届かない対象に使う。
JS/TS では @jazzer.js や jsfuzz がカバレッジ計装つきのファザーを提供する。
分岐が浅く入力空間も狭いなら、計装のコストに見合わないので通常のファジングや PBT で足りる。
TypeScript example
@jazzer.js のファズターゲットの最小形(専用ランナー jazzer で起動し、コーパスを育てる)。
import { FuzzedDataProvider } from "@jazzer.js/core";
import { parseConfig } from "../config";
export function fuzz(data: Buffer) {
const provider = new FuzzedDataProvider(data);
const raw = provider.consumeRemainingAsString();
const r = parseConfig(raw);
if (r.kind !== "ok" && r.kind !== "error") throw new Error("invalid result");
}
落とし穴
- 計装つき実行は通常のユニットテストと別ランナー、別 CI ジョブになる。常時 CI に乗せるか、夜間ジョブに回すか先に決める。
- 育てたコーパスを捨てると毎回ゼロから探索になる。コーパスは保存して回帰の種に使う。
遂行手順(着手→完了)
CGF はファジングに「カバレッジ計測によるコーパス育成」を足したものだ。到達分岐の計測と打ち切り判定が手順の中核になる。
- 計装つきランナーを用意する:
@jazzer.js(libFuzzer ベース)や jsfuzz を入れ、対象モジュールがカバレッジ計装の対象に入るよう設定する。通常の vitest とは別ランナー・別エントリ(*.fuzz.ts)になる。
- ファズターゲットを定義する:
fuzz(data: Buffer) で生バイトを受け、FuzzedDataProvider で対象の入力型へ変換し、対象を1回呼ぶ。ファジングと同じく不変条件違反は throw してクラッシュ扱いにする(壊れの定義を据える)。
- 永続コーパスディレクトリを用意する:
corpus/ を引数に渡し、ファザーがそこに新規経路を開いた入力を溜める。これを git か CI アーティファクトで保存し、回帰の種にする(捨てると毎回ゼロ探索)。
- 初期種を投入する:実データ・既知のバグ入力・仕様上の代表例を
corpus/ に置いてから走らせる。種があると分岐の深部へ早く届く。
- カバレッジが頭打ちになるまで回す:ランナーが報告する到達分岐数(libFuzzer の
cov: / 新規 feature 検出)を見て、新規経路を開く入力が枯れる(カバレッジが伸びなくなる)まで走らせる。-max_total_time か -runs で1回の上限を切る。
- 打ち切りを2系統で決める:CI の通常ジョブは短時間(例 60 秒)で回帰チェックに使い、深い探索は夜間/週次の長時間ジョブに分ける。どちらに何を載せるかを先に決める(計装ジョブを常時 CI に重く載せると遅くなる)。
- クラッシュの最小化と回帰固定:ファザーが吐くクラッシュ入力(
crash-*)を最小化(-minimize_crash=1 等)し、コーパスと回帰テストに固定する。最小化した入力を通常の例ベーステストにも落とし、計装無しでも再現できるようにする。
完了チェック(もれ確認)
- 計装ジョブが分離されているか:CGF が通常のユニットテストと同じランナー/ジョブに混ざっていないか。別ランナー・別 CI ジョブになっているか確認する。
- コーパスが永続化されているか:
corpus/ が保存(git/アーティファクト)され、次回の種として再利用されるか。捨てていれば毎回ゼロ探索で深部に届かない。
- カバレッジ頭打ちで打ち切ったか:到達分岐数が伸びなくなるまで回したか、それとも適当な時間で止めたか。打ち切り根拠(時間 or カバレッジ飽和)が明示されているか。
- 不変条件が throw に落ちているか:ファズターゲット内で、壊れの定義(不変条件違反)が
throw でクラッシュ扱いになっているか。return で握り潰していないか。
- クラッシュが最小化・回帰固定されたか:見つかったクラッシュ入力を最小化し、計装無しでも再現する回帰テストに落としたか。
- 短時間ジョブと長時間ジョブの役割が決まっているか:CI 用(短)と探索用(長)の二系統の打ち切り時間が決まっているか。
網羅設計での効かせ方
CGF の網羅対象は「到達したコードカバレッジ(分岐・パス)」そのものであり、これを最大化することが網羅の駆動力になる。
素朴なファジングがランダムに入力を撒くのに対し、CGF はカバレッジを報酬にして「まだ踏んでいない分岐を開く入力」を残し変異させるので、深い分岐を持つ対象で網羅が桁違いに伸びる。
ただし測れるのは構造カバレッジ(到達した分岐)であって、意味的正しさではない。網羅基準は「新規経路を開く入力が枯れる(カバレッジが頭打ち)まで探索し、育てたコーパスで近似」と置く。
コーパスは育てた網羅の保存形そのものなので、捨てると網羅がリセットされる。永続化が網羅の連続性を担保する。
壊れの定義(ファズターゲット内の不変条件)が、その網羅で何を検出できるかを決める。カバレッジを稼いでも不変条件が薄ければ「踏んだだけ」で終わる。
配分の原則は、分岐が深く素朴な乱数では奥へ届かない対象だけ CGF に寄せ、浅い対象は通常ファジング/PBT に留めることだ(計装コストに見合わない網羅は取らない)。
性質・組合せで縛る系は別ファイルへ
入力全体に成り立つ性質で縛る(PBT)、パラメータの組合せを covering array で覆う(コンビナトリアル)系は、generative-property.md にまとめた。
oracle を別の参照で代替する手法は別ファイルへ
正解を直接持たないまま、別の参照や関係で正しさを判定する手法は、過去出力基準(スナップショット、承認テスト、仕様化テスト)を oracle-past-output.md、別実装基準(差分テスト)を oracle-differential.md、関係/モデル基準(メタモルフィックテスト、モデルベーステスト、形式検証連携)を oracle-relational.md にまとめた。
AI と非決定的出力は別ファイルへ
テストを書く道具としての LLM(AI/LLM 支援テスト生成)と、製品に組み込まれた非決定的出力そのものの検証(階層化品質設計)は、ai-nondeterministic.md にまとめた。