jonpurvis/fuzz
Fuzz is a Laravel package for fuzz testing your application. It sends randomized requests and payloads to your routes, helping uncover unhandled exceptions and 500 errors before they reach production.
Install as a dev dependency.
composer require jonpurvis/fuzz --dev
Pest plugins that declare extra.pest.plugin in their composer.json are auto-registered — after composer dump-autoload you should see the ->fuzz() method available without editing tests/Pest.php. If not, check the README for the manual uses() registration line.
Enable a coverage driver. Coverage-guided fuzzing is the whole point of the package, so it needs real coverage feedback:
pecl install pcov # preferred for speed
# or ensure Xdebug is installed with coverage mode available
Confirm with php -m | grep -E 'pcov|xdebug' and vendor/bin/pest --coverage before writing your first fuzz test — if coverage is broken, the fuzzer either errors or silently degrades to blind random testing.
Look here first, in order:
vendor/jonpurvis/fuzz/README.md — the only reliable source of the exact API (config keys, seed handling, iteration limits).vendor/jonpurvis/fuzz/tests/ — real, executable examples. Faster than the docs.vendor/jonpurvis/fuzz/config/ — publish with php artisan vendor:publish --tag=fuzz-config if a config file is shipped.vendor/jonpurvis/fuzz/src/ — the mutator/corpus classes tell you exactly what input shapes are supported.First use case — a pure parser/validator. Fuzzing pays off immediately on code that takes untrusted scalars and does branching:
it('never fatals on arbitrary input', function (string $input) {
$result = (new Slugger())->slug($input);
expect($result)->toBeString()
->and($result)->not->toMatch('/\s/');
})->fuzz();
Run it with vendor/bin/pest --filter="never fatals". Failures print the input (and seed where supported) that broke the invariant — that input is your regression test.
Invariant-first assertions, not exact-output assertions. Fuzzing can't know the "right" answer, so assert properties: round-trips, bounds, idempotency, "no exception of type X", "output is always a subset of input".
it('round-trips safely', function (string $s) {
expect(base64_decode(base64_encode($s), true))->toBe($s);
})->fuzz();
Type-hinted parameters drive generation. The fuzzer inspects your test closure's parameters and produces inputs matching the declared types (string, int, array, etc.). This is the primary extension point — if you need a domain object, accept the primitive and construct the object inside the test body:
it('handles any email-ish string', function (string $raw) {
$email = new EmailAddress($raw); // your constructor does the validating
expect($email->domain())->toBeString();
})->fuzz();
Corpus-driven workflow for real bugs. Coverage-guided fuzzers keep inputs that hit new branches in a corpus directory. Commit that corpus (or persist it in CI cache) so each run starts where the last one ended — that is what turns a 30-second test into a cumulative bug hunter.
Isolate from the rest of your suite.
// tests/Pest.php
uses()->group('fuzz')->in('Fuzz');
Then: default vendor/bin/pest stays fast and deterministic; vendor/bin/pest --group=fuzz runs on demand or on a nightly CI schedule.
Cap the runtime for CI. Coverage-guided runs are open-ended. Prefer an iteration/time budget from the config file over "run until it finds something", and fail the job only on an actual crash so a long run doesn't red-flag a PR.
Layer it under existing unit tests. Keep your example-based tests for behavior; add one or two fuzz tests per "untrusted input" boundary (HTTP payload parsing, markdown/HTML sanitizers, CSV/JSON deserializers, money/number formatting, regex-heavy string utilities). Those are where coverage-guided mutation finds off-by-one and encoding bugs.
Reproduce failures deterministically. When a fuzz run fails, copy the reported input into a normal it('regression: input X', ...) test before fixing. The fuzzer will find the same class of bug again; the regression test makes sure it doesn't.
Pair with shrinking. Most fuzzers shrink a failing input to a minimal reproducer. Let it — a 12-byte failing string is a far better bug report than a 4KB one. Don't manually truncate the reported case.
pest run. A flaky fuzz failure at 2am on an unrelated PR erodes trust in the whole suite. Gate behind --group=fuzz or a FUZZ=true env var.DB::transaction() + rollback and use RefreshDatabase; even then, expect it to crawl.once()-memoized config persist between generated inputs. Reset anything stateful in the test body or a beforeEach.$input parameter typically yields strings only. If you want integers or arrays, declare them — don't coerce inside the test, or you're fuzzing your coercion, not your code.memory_limit. Raise it for the fuzz job specifically (php -d memory_limit=1G vendor/bin/pest --group=fuzz).assert() and expect() are your crash signal. A thrown exception is the normal failure mechanism; the fuzzer records the input. Use ->not->toThrow() deliberately for "should never throw" invariants, and plain assertions with precise messages for everything else.vendor/jonpurvis/fuzz/src/ before writing your own generator — the mutators are usually the intended hook.php-fuzzer or property-testing libraries — it's a lightweight Pest adapter for targeted invariants.How can I help you explore Laravel packages today?