hughsaffar/laravel-test-trap
Laravel Test Trap is a focused testing helper for Laravel that adds guardrails to your suite, helping catch unintended behavior before it reaches CI. It keeps tests safer and more predictable with minimal setup, making it a useful addition for teams tightening test reliability.
Install as a dev dependency only. This is a test-time utility — never let it into production:
composer require --dev hughsaffar/laravel-test-trap
Look at the config first. After install, check vendor/hughsaffar/laravel-test-trap/config/ (or publish with php artisan vendor:publish --tag=test-trap-config if the package registers a tag). The two knobs you care about are the max test duration (milliseconds/seconds) and the slow query threshold plus any query-count limits. Set these deliberately rather than accepting defaults — defaults are usually tuned for a "normal" app and will either be too noisy or too loose for yours.
First use case: run your existing suite unchanged:
php artisan test
You should see per-test timing and any slow-query output flagged after the normal PHPUnit/Pest summary. If you see nothing, confirm the package's service provider is auto-discovered (php artisan about or check bootstrap/cache/packages.php) — a missing provider is the most common "it does nothing" cause.
If the package exposes a trait or base test case, apply it to your project's TestCase so both feature and unit tests inherit the behavior:
// tests/TestCase.php
use Hughsaffar\TestTrap\Traits\TrapsTests; // verify exact namespace in vendor/
abstract class TestCase extends BaseTestCase
{
use TrapsTests;
}
Start by running one slow-ish test suite (e.g. your feature tests hitting MySQL) rather than the whole suite, so the output is readable and you can tune thresholds against a real baseline.
Set a realistic duration budget per suite. Don't apply one global timeout across unit and integration tests — unit tests should be near-instant, feature tests tolerate more. If the package config is static, use phpunit.xml <env> overrides or @group separation so you can run CI with strict thresholds and local dev with looser ones:
<!-- phpunit.xml -->
<env name="TEST_TRAP_MAX_MS" value="200"/>
(Confirm the actual env keys in the package config — substitute accordingly.)
Use it as a CI gate on the SQL side, not the timing side. Timing thresholds are flaky on shared CI runners; query-count and slow-query detection is deterministic and far more valuable. A common pattern:
Catch N+1 regressions by asserting query counts on hot paths. Wrap performance-sensitive tests:
public function test_dashboard_loads_without_n_plus_one(): void
{
$this->assertQueryCountLessThan(15, function () {
$this->actingAs($this->user)->get('/dashboard')->assertOk();
});
}
If the package provides its own assertion helpers, prefer those; otherwise pair it with Laravel's DB::listen counter. The trap's value here is that it fails rather than merely printing (unlike DB::enableQueryLog() inspection after the fact).
Integration point for Pest: apply the trait via uses(TrapsTests::class)->in('Feature'); in tests/Pest.php if you're on Pest, so you don't touch every file.
Tag the noisy tests. If a specific test is legitimately slow (a large fixture import, a report generation), give it @group slow and exclude that group from the trap run in CI, keeping the trap honest for the 95% of fast tests.
Maintenance risk is real. 18 stars, one release (2023-06-28), low assessment score (0.22). Pin the version in composer.json, and be prepared to fork — the whole feature set is small enough to reimplement with a DB::listen listener and a PHPUnit extension in an afternoon if the package stops working on a future Laravel version. Do not build critical CI infrastructure on assumptions about its longevity.
Timing traps are the biggest false-positive source. Slow CI runners, Xdebug being enabled, and cold caches will trip duration thresholds intermittently. If the trap is failing on timing:
XDEBUG_MODE=off).Query logging and the query trap interact badly with transactions. If you use RefreshDatabase with transactions (the default) and also assert query counts, the BEGIN/COMMIT statements may or may not be counted depending on the package's listener placement. Establish a baseline assertion on a trivial test first, then build from there — off-by-one query counts are usually transaction noise, not your code.
"Problematic SQL" detection needs a definition. If the package flags queries by duration or by pattern (e.g. SELECT * without WHERE), confirm which — the label is vague. If it's duration-based, your slow-query threshold and your test-trap threshold are two independent settings; tune them separately. Set the slow-query threshold at whatever your staging DB says is normal + margin, not an arbitrary 100ms.
Extension points to lean on: if the package exposes an event, a reporter, or a custom assertion trait, use those rather than wrapping test methods yourself — wrappers defeat the point of automatic trapping and drift from the package's reporting format.
Debugging "it didn't trap": verify (a) the trait is actually applied to the test class (an inherited base TestCase is fine, but a test extending PHPUnit\Framework\TestCase directly won't get it), (b) the service provider registered, and (c) you aren't overriding the package config in phpunit.xml with an empty/zero value, which some config systems interpret as "disabled" rather than "unlimited."
Don't let it mask real failures. If a test legitimately needs 300ms and your trap is at 200ms, fix the threshold — but if the trap is what made you notice the 300ms test, follow the scent: it's often an uncached Eloquent relationship or a missing index in a factory-seeded query. The trap's best use is as a tripwire that prompts investigation, not as a number to silence.
How can I help you explore Laravel packages today?