Pestのファズテスト入門:Laravelで想定外の入力を探す実践手順

多様な入力データが分岐経路を進み、未検証の経路を照らすファズテストの概念図

Laravelアプリが外部から受け取る文字列には、開発者が手で列挙したテストケースを越える組み合わせがあります。
空のJSON、途中で切れた値、想定外の型、重複したキーなどは、正常系のテストだけでは見落としやすい入力です。

Laravel Newsが紹介したFuzzは、既知の入力を少しずつ変化させ、未実行の分岐へ進んだ入力を優先して試すPest 5向けパッケージです。
この記事では、Laravelの入力処理へ安全に試すための範囲と、見つかった失敗を継続的な品質改善へつなげる手順を扱います。

カバレッジ誘導型ファズテストの仕組み

ファズテストは、プログラムへ多数の変形入力を与え、クラッシュや守るべき条件の違反を探すテスト手法です。
Fuzzはnikic/PHP-Fuzzerを内部で利用し、入力が通過したコードの違いを探索の手掛かりにします。

単なるランダム入力では、最初の形式チェックで拒否される値ばかり増えることがあります。
カバレッジ誘導型では、新しい分岐へ到達した入力をライブラリへ残して再び変形するため、パーサーの奥にある条件へ探索を進めやすくなります。

手法 得意な問い 残る限界
Pestのデータセット 列挙した入力が期待どおりに動くか 開発者が思い付かなかった入力は試されない
ファズテスト 限られた実行回数で未想定の失敗入力を見つけられるか 通過しても不具合がないとは証明できない
機能テスト HTTPやDBを含む利用経路が成立するか 入力空間を網羅するには個別設計が要る

Pest公式のデータセットは、既知の正常値や異常値を名前付きで固定する用途に向きます。
ファズテストと競合する仕組みではありません。
探索で見つけた入力をデータセットへ昇格させることで、探索と再発防止を分担できます。

導入前に確認する実行条件

Packagistのjonpurvis/fuzzページによると、現行の安定版はPHP 8.4以降、Pest 5系、nikic/php-fuzzer 0.0.11を依存条件とします。
既存プロジェクトがPHP 8.3以前やPest 4以前なら、テスト用依存関係だけを無理に上げず、アプリ本体を含む更新計画を先に立てます。

このFuzzはLaravel本体やPest本体の標準機能ではなく、コミュニティが公開する若いパッケージです。
導入時にはFuzzの公式GitHubリポジトリでREADME、リリース、変更履歴、未解決の課題を確認し、使用するバージョンをComposerで固定します。

composer require jonpurvis/fuzz --dev

Laravel公式のテスト資料は、LaravelがPestとPHPUnitを標準で利用でき、UnitテストとFeatureテストを分けていると説明しています。
最初のファズ対象には、Laravel全体を起動しなくても呼べる小さなパーサーや変換処理を選ぶと、失敗原因を特定しやすく、実行時間も管理しやすくなります。

Laravelで試しやすい入力処理

ファズテストは、入力の候補を人手で書き切れない処理に向きます。
Laravelアプリでは、次のような境界が候補になります。

  • WebhookのJSONからイベント名や識別子を取り出す処理
  • CSVの一行や固定形式のコードを分解する処理
  • 検索条件や並び順の文字列を内部表現へ変換する処理
  • Markdownや独自記法を解析する処理
  • ファイル名や外部APIの値を正規化する処理

反対に、決済の実行、メール送信、外部APIへの書き込みをそのまま対象へ含めるべきではありません。
ファズテストは同じ処理を何度も呼ぶため、副作用を分離できていない対象では、テストデータの増加や外部サービスへの誤送信を招きます。

最小のテストを組み立てる

次の例では、Webhookの本文からイベント名を取り出す自作クラスを対象にします。
Fuzzのワーカーから呼び出しやすくするため、対象のクロージャーはテスト本体の外で定義し、Laravelのテストケースに結び付いた$thisへ依存させません。

<?php

use App\Support\WebhookEventName;
use function Fuzz\fuzz;

$target = static function (string $input): void {
    $eventName = WebhookEventName::parse($input);

    expect($eventName)->toBeString();
    expect($eventName)->not->toBeEmpty();
};

test('webhookの入力で致命的な失敗を起こさない', function () use ($target): void {
    fuzz($target)
        ->seed([
            '{"event":"invoice.paid"}',
            '{"event":"checkout.completed"}',
        ])
        ->withDictionary(['{', '}', '[', ']', ':', ',', 'null', 'event'])
        ->runs(1000)
        ->maxLen(256)
        ->run('webhook-event-name');
});

seed()には、対象の深い分岐まで届く既知の入力を置きます。
withDictionary()にはJSONの記号や業務上のキー名など、入力形式に関係する断片を渡します。
runs()は対象を呼ぶ上限回数、maxLen()は生成する入力の最大バイト数です。

例の期待値は、戻り値が空でない文字列になるという不変条件を表します。
エラーを起こさないことだけを確認すると、誤った値を静かに返す不具合は通過するため、対象が常に守るべき性質も期待値にします。

ただし、すべての不正入力からイベント名を返す設計が正しいとは限りません。
不正なJSONを明示的な例外として拒否する契約なら、許容する例外の種類を決め、そのほかの例外やエラーを失敗として扱うようにテストを調整します。

失敗入力を回帰テストへ残す

Fuzzは、既定で失敗した入力を.pest/fuzz-crashes/配下へ保存します。
ファズテストが失敗したら、次の順で処理します。

  1. 保存された入力を使い、対象メソッドだけで失敗を再現する
  2. 例外の種類、スタックトレース、入力のどの性質が契約に反したかを確認する
  3. 入力検証または処理本体を修正し、同じ入力で期待する結果を決める
  4. 失敗入力を名前付きデータセットへ追加し、通常のテストで毎回確認する
  5. ファズテストを再実行し、別の分岐に失敗が残っていないか探索する

失敗入力には制御文字やバイナリが含まれる場合があります。
内容をそのまま端末、HTML、ログへ出力せず、エスケープして長さを制限し、機密情報を含まないテスト環境で扱います。

Laravelのテスト全体を整理する場合は、Laravelのテスト戦略でUnit、Feature、DB、外部連携の役割も確認できます。
探索で得た一件を再発防止へ固定する流れは、Laravelで実践するTDDとも接続できます。

CIへ組み込むときの予算設計

ファズテストは、実行回数を増やすほど探索の機会が増える一方、通常のテストより結果と所要時間が変動します。
プルリクエストでは小さな実行回数と入力長に抑え、長時間の探索は定期実行へ分けると、開発者の待ち時間と探索量を両立しやすくなります。

  • 対象を限定する:一つのファズテストで一つの入力境界を扱う
  • 名前を固定する:run()へ用途が分かる名前を渡し、保存領域を区別する
  • 予算を記録する:実行回数、最大入力長、タイムアウトをコードレビューの対象にする
  • 失敗成果物を保全する:CIのクラッシュ入力を安全な成果物として回収できるようにする
  • 通常テストへ戻す:再現した失敗はデータセットへ固定し、偶然の再発見に頼らない

「1000回通った」は「すべての入力に正しい」を意味しません。
その回の探索予算内で、設定した失敗条件を満たす入力が見つからなかったという結果です。
対応範囲は、通常のUnitテスト、Featureテスト、静的解析、コードレビューと組み合わせて広げます。

採用判断のチェックリスト

  • PHP 8.4以降とPest 5へ安全にそろえられるか
  • 外部入力を受ける処理を副作用のない小さな関数へ分離できるか
  • 戻り値や状態が守るべき不変条件を一文で説明できるか
  • 期待する業務例をseed、形式上の断片をdictionaryとして用意できるか
  • クラッシュ入力を回収し、修正後にデータセットへ残す担当を決めているか
  • コミュニティパッケージの更新と互換性を継続して確認できるか

一つでも未決なら、まず通常のテストで入力契約を明文化します。
ファズテストは、何を正しいとするかが決まっている処理へ追加して初めて、想定漏れを探す結果を判断できます。

よくある質問

ファズテストはセキュリティ診断の代わりになりますか

代わりにはなりません。
設定した対象でクラッシュや不変条件違反を探せますが、認証、権限、設定、依存パッケージ、実際のHTTP経路を含む診断を一括して保証する仕組みではありません。

データセットを削除してFuzzへ置き換えてよいですか

置き換えません。
名前付きデータセットは仕様と既知の回帰を固定し、Fuzzは未知の候補を探索します。
見つかった失敗をデータセットへ加える運用で両者を併用します。

LaravelのFeatureテスト全体を対象にできますか

技術的な組み込み可否だけで判断せず、ワーカー分離、DB初期化、副作用、実行時間を先に検証します。
初回は純粋なパーサーや正規化処理から始め、必要なLaravel機能を一つずつ増やす方が原因を追いやすくなります。

導入の結論

Fuzzは、Laravelアプリの入力処理で「開発者が思い付いた例」から「未実行の分岐を探す探索」へテスト範囲を広げる選択肢です。
PHPとPestの依存条件を満たし、対象を小さく分離し、失敗入力を通常テストへ固定する運用まで用意できるなら、パーサーや外部連携の想定漏れを早い段階で見つけやすくなります。

参考情報

この記事に関連する株式会社greedenの取り組み

入力の想定漏れを減らすには、検証手法だけでなく対象範囲と再現手順を開発工程へ組み込む設計が必要です。株式会社greedenは、要件定義からテスト、保守改善まで一貫したWebシステム開発を支援しています。

投稿者 greeden Inc.

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)