johnkary/phpunit-speedtrap
PHPUnit SpeedTrap reports slow tests and slow test suites so you can find and fix performance bottlenecks. Set a time threshold and it flags any test exceeding it, helping keep your suite fast.
1. Install as a dev dependency
composer require --dev johnkary/phpunit-speedtrap
2. Confirm your PHPUnit version before going further
composer show phpunit/phpunit | grep versions
This is the single most important step. SpeedTrap's latest tagged release (2021) ships a PHPUnit listener, and listeners were removed in PHPUnit 10. So:
^9.5) — works as documented.3. Register the listener in phpunit.xml
<phpunit ...>
<listeners>
<listener class="JohnKary\PHPUnit\Listener\SpeedTrapListener">
<arguments>
<array>
<element key="slowThreshold">
<integer>500</integer>
</element>
<element key="reportLength">
<integer>10</integer>
</element>
</array>
</arguments>
</listener>
</listeners>
</phpunit>
4. Run your suite and look at the tail of the output
php artisan test
# or
vendor/bin/phpunit
After the normal result line you should get a "slow tests" block listing each test above the threshold, its duration, and a count of how many times it exceeded the threshold. If you see nothing, jump to Gotchas and Tips — silent non-configuration is the #1 failure mode.
First use case: run the full suite with slowThreshold at 1000 to get a short list of the genuinely bad offenders, fix the top 3, then ratchet the threshold down to 500.
Start permissive, then ratchet. A 500 ms threshold on a legacy Laravel suite produces dozens of entries and the team stops reading the report. Begin at 1000–1500 ms, fix or annotate the top offenders, and commit a lower threshold only once the report is short. Treating the number in phpunit.xml as a reviewed budget (like a coverage minimum) is what makes the tool keep working after month three.
Per-test override for intentionally heavy tests. Anything that legitimately needs >1 s — PDF generation, spreadsheet exports, image manipulation, a seeded full-database import — should declare its own budget in the docblock rather than pollute the report:
/**
* @slowThreshold 3000
*/
public function test_generates_quarterly_pdf_report(): void
{
//
}
Add a one-line comment explaining why it's heavy. Otherwise the next developer "fixes" the annotation instead of the test.
Typical Laravel culprits and the fix each one implies
| Symptom | Usual cause | Fix |
|---|---|---|
| 300–800 ms on every DB test | migrations running per test | RefreshDatabase with a schema-dump/migration cache, or DatabaseTransactions |
| 500 ms+ on model tests | factories with ->create() and deep relationships |
build only what the test asserts; use make()/states; createMany |
| 1 s+ sporadic | real HTTP in a feature test | Http::fake(), or move to a contract test |
| 2 s+ | Hash::make() at production bcrypt rounds |
BCRYPT_ROUNDS=4 in phpunit.xml env |
| Slow, uniform | Storage::disk('local') writes |
Storage::fake() |
| Very slow, no obvious cause | sync queue driver running listeners | Queue::fake() unless the test is about the job |
Iterate on one test at a time. Once SpeedTrap names an offender, measure in isolation:
vendor/bin/phpunit --filter test_generates_quarterly_pdf_report
Repeat after each change. SpeedTrap tells you what is slow, not why — for the "why" within a slow test, reach for Xdebug profiling or a few microtime(true) checkpoints temporarily.
CI vs local configuration. Keep two thresholds: a strict one locally (where hardware is predictable) and a looser one in CI (shared runners are noisy). Recent versions support environment-variable overrides for exactly this, so the committed phpunit.xml can hold the local value and CI can export the looser one — check the README for the exact variable names for your installed tag, and verify the report actually quotes your new number before trusting it.
Use it as a regression guard, not just a cleanup tool. Because the listener reports on every run, a threshold committed to the repo makes a newly-slow test visible in the same PR that introduces it. Pair this with PHPUnit's failure output in CI logs so reviewers see the slow-test block without digging.
Split the suite rather than annotating everything. If a meaningful chunk of tests are inherently slow, put them in @group integration / a separate phpunit.integration.xml and run SpeedTrap only against the fast suite during the inner dev loop. Annotations are for exceptions; groups are for categories.
<array>/<element> nesting is mandatory. Writing <integer>500</integer> directly under <arguments> (a very natural-looking mistake) is silently ignored — SpeedTrap falls back to its defaults and you'll believe it's working while reporting against the wrong number. Verify by temporarily setting the threshold to 1 and confirming the report explodes with entries.composer.json PHPUnit constraint first.0.5 or "500ms" will not do what you expect.setUp()/tearDown() for that test, so a slow setUp() shows up as a slow test method. setUpBeforeClass() overhead is amortised across the class and may be attributed in ways that surprise you — a class that looks uniformly slow often has one expensive bootstrap.12x next to a test usually means the provider, not repeated runs.@slowThreshold is inert, and in Pest the docblock above it(...) may not be picked up reliably — prefer the global config value there.--log-junit output, a memory-limit-per-test listener, or memory_get_peak_usage() assertions — this package won't help there.--no-output, some CI log formatters, and custom test printers can hide the listener's block. If you suspect this, run the single slow test directly with vendor/bin/phpunit --filter ... and watch stdout.JohnKary\PHPUnit\Listener\SpeedTrapListener) — you can subclass it to change the report header/format or to pipe offenders to a file or Slack notification, then register your subclass in phpunit.xml instead of the vendor class.How can I help you explore Laravel packages today?