Fuzz ist ein Paket von Jon Purvis, das coverage-gesteuertes Fuzz-Testing in Pest 5 bringt. Es baut auf nikics PHP-Fuzzer auf, mutiert vorgegebene Strings und führt sie dem Code zu, bis es einen Fehler findet oder das Lauf-Budget erschöpft ist. Ein bestandener Lauf bedeutet nur, dass in diesen Versuchen kein Fehler gefunden wurde.

tests/Unit/RateLimitTest.php
use App\RateLimit;
use function Fuzz\fuzz;

$target = static function (string $input): void {
    RateLimit::perSecond($input);
};

test('rate limit spec parser never fatals', function () use ($target): void {
    fuzz($target)
        ->seed(['100/60s', '5/1s', '1000/3600s'])
        ->withDictionary(['/', 's', '0', '1'])
        ->runs(2000)
        ->maxLen(16)
        ->run('rate-limit-parser');
});

Ein coverage-gesteuerter Fuzzer beobachtet, welche Pfade Eingaben durch den Code nehmen. Erreicht eine Eingabe einen bisher unerforschten Pfad, speichert der Fuzzer sie in einem Corpus und erzeugt daraus weitere Varianten. PHP-Fuzzer sammelt dieses Feedback, indem es Übergänge zwischen PHP-Codeblöcken und deren ungefähre Ausführungshäufigkeit verfolgt. Fuzz übernimmt die Instrumentierung selbst, Xdebug und Pests Option --coverage sind nicht nötig.

Fuzz setzt PHP 8.4+ und Pest 5 voraus. Installiert wird es mit `composer require jonpurvis/fuzz --dev`. Die Anleitung nutzt einen RateLimit-Helper, der eine Angabe wie 100/60s in Anfragen pro Sekunde umrechnet, die Eingabe aber nicht validiert. Im Test wird eine Zielfunktion, die den Helper aufruft, außerhalb von test() definiert. Dann wird fuzz() mit seed(), withDictionary(), runs(2000), maxLen(16) und einem eindeutigen Namen in run() konfiguriert, den Fuzz zur Trennung gespeicherter Eingaben und Crash-Dateien verwendet.

Die Definition außerhalb von test() ist wichtig: Bei der Prüfung von Fuzz v1.0.1 zeichnete der Wrapper Coverage auf, Closure::fromCallable() dagegen nicht. Grund ist, dass Fuzz die Funktion in einem separaten PHP-Prozess ausführt, in dem Pests generierte Testklasse nicht verfügbar ist.

Im Lauf des Autors fand Fuzz die Eingabe 5/. Das fehlende Zeitfenster wird zu einem leeren String. PHP wandelt ihn in 0 um, und die Division wirft einen DivisionByZeroError. Pest meldet den Test als fehlgeschlagen. Fehlschlagende Eingaben werden standardmäßig unter .pest/fuzz-crashes/ gespeichert. Nach der Korrektur empfiehlt es sich, die Eingabe in ein benanntes Dataset aufzunehmen und das erwartete Verhalten zu prüfen.

Abstürze sind nicht die einzigen relevanten Fehler. Ein falscher Rückgabewert kann unbemerkt durchgehen, daher lässt sich eine Pest-Expectation in die Zielfunktion einbauen, etwa dass Kodieren und anschließendes Dekodieren den Originalstring ergibt. Fuzz meldet TypeError sowie nicht unterdrückte PHP-Warnungen und Notices. Gewöhnliche Exceptions, einschließlich Laravel-Validation-Exceptions, werden standardmäßig ignoriert. Die Methode allow() engt diese Liste ein, und timeout() setzt ein Limit pro Eingabe, was die pcntl-Erweiterung erfordert.

Der Autor rät, reguläre Tests und Datasets für bekannte Fälle beizubehalten. Fuzz lohnt sich, wenn Code mehr Eingaben akzeptiert, als man sinnvoll auflisten kann, etwa Parser für Benutzereingaben, besonders bei Parsern mit aufeinanderfolgenden Validierungsschritten. Empfohlen wird ein kleines Lauf-Budget in der normalen Suite und längere Suchläufe in einem geplanten CI-Job.