Fuzz es un paquete de Jon Purvis que lleva el fuzzing guiado por cobertura a Pest 5. Se basa en PHP-Fuzzer de nikic, muta las cadenas proporcionadas y las pasa al código hasta encontrar un fallo o agotar el presupuesto de ejecución. Una ejecución exitosa solo significa que no se encontró ningún fallo en esos intentos.

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');
});

Un fuzzer guiado por cobertura observa qué rutas toman las entradas por el código. Cuando una entrada alcanza una ruta inexplorada, el fuzzer la guarda en un corpus y la usa para crear más variaciones. PHP-Fuzzer recoge esta retroalimentación siguiendo las transiciones entre bloques de código PHP y su frecuencia aproximada de ejecución. Fuzz se encarga de la instrumentación, por lo que no se necesitan Xdebug ni la opción --coverage de Pest.

Fuzz requiere PHP 8.4+ y Pest 5. Se instala con `composer require jonpurvis/fuzz --dev`. El tutorial usa un helper RateLimit que convierte una especificación como 100/60s en peticiones por segundo sin validar la entrada. En el test, una función objetivo que llama al helper se define fuera de test(), y luego fuzz() se configura con seed(), withDictionary(), runs(2000), maxLen(16) y un nombre único en run(), que Fuzz usa para separar las entradas guardadas y los archivos de fallo.

Definir la función fuera de test() es importante: en la comprobación de Fuzz v1.0.1, ese wrapper registró cobertura mientras que Closure::fromCallable() no registró ninguna, porque Fuzz ejecuta la función en un proceso PHP separado donde la clase de test generada por Pest no está disponible.

En la ejecución del autor, Fuzz encontró la entrada 5/. La ventana ausente se convierte en una cadena vacía. PHP la convierte a 0 y la división lanza una DivisionByZeroError. Pest reporta el test como fallido. Las entradas fallidas se guardan por defecto en .pest/fuzz-crashes/. Tras corregir el parser, la práctica recomendada es añadir la entrada a un dataset nombrado y verificar el comportamiento esperado.

Los fallos no son lo único que conviene detectar. Un valor de retorno incorrecto puede pasar desapercibido, así que se puede añadir una expectation de Pest dentro de la función objetivo, por ejemplo comprobar que codificar y luego decodificar una cadena devuelve la original. Fuzz reporta TypeError y los avisos y notices de PHP no suprimidos. Las excepciones ordinarias, incluidas las de validación de Laravel, se ignoran por defecto. El método allow() reduce esa lista, y timeout() fija un límite por entrada, lo que requiere la extensión pcntl.

El autor aconseja mantener los tests y datasets habituales para los casos conocidos. Fuzz resulta útil cuando el código acepta más entradas de las que se pueden enumerar razonablemente, como parsers que leen texto del usuario, sobre todo con pasos de validación sucesivos donde la cobertura guía al fuzzer hacia código más profundo. Recomienda empezar con un presupuesto pequeño en la suite normal y mover las búsquedas largas a un trabajo de CI programado.