ergebnis/phpunit-slow-test-detector
Detect slow PHPUnit tests with an extension delivered as a Composer package or PHAR. Configure a global maximum duration and get a report of tests exceeding the threshold after each run—ideal for catching performance regressions in your suite.
composer require --dev ergebnis/phpunit-slow-test-detector
phpunit.xml:
Add the extension to your PHPUnit config based on your PHPUnit version (see README for exact syntax).
Example for PHPUnit 10+:
<extensions>
<bootstrap class="Ergebnis\PHPUnit\SlowTestDetector\Extension"/>
</extensions>
./vendor/bin/phpunit
The extension will now report slow tests (default threshold: 500ms).Run your test suite as usual. The extension will:
Example output:
Detected 3 tests where the duration exceeded the global maximum duration (0.500).
# Duration Test
1 1.200 App\Tests\Feature\SlowIntegrationTest::testPaymentProcessing
2 0.850 App\Tests\Unit\LegacyDatabaseTest::testDeprecatedQuery
CI/CD Pipeline:
maximum-count=5).- run: ./vendor/bin/phpunit --configuration=phpunit-slow.xml
Where phpunit-slow.xml includes:
<parameter name="maximum-count" value="3" type="int"/>
<parameter name="maximum-duration" value="300" type="int"/>
Team Onboarding:
@slow PHPDoc tags).Test Suite Optimization:
--filter=SlowTestClass to isolate slow suites.Per-Environment Thresholds:
Use environment variables to override defaults in phpunit.xml:
<parameter name="maximum-duration" value="${env.SLOW_TEST_THRESHOLD:-500}" type="int"/>
Set in .env:
SLOW_TEST_THRESHOLD=300 # Stricter in CI
Test Suite-Specific Rules: Override thresholds for specific test suites:
<testsuites>
<testsuite name="integration">
<directory>tests/Integration/</directory>
<parameter name="maximum-duration" value="1000" type="int"/>
</testsuite>
</testsuites>
Stderr Output: Redirect slow test reports to stderr for CI logs:
<parameter name="stderr" value="true" type="bool"/>
Database-Driven Tests:
DatabaseMigrations or Feature\Payment tests.Queue/Job Tests:
// tests/Feature/PaymentTest.php
public function test_payment_processing() {
$this->withoutExceptionHandling();
// Mock slow API call
$this->partialMock(Stripe::class, function ($mock) {
$mock->shouldReceive('charges.create')->andReturnUsing(fn() => sleep(1));
});
}
Artisan Command Tests:
Artisan::call() tests to avoid real filesystem/database operations.// tests/Feature/CommandTest.php
public function test_slow_command() {
Storage::fake(); // Avoid real filesystem I/O
$this->artisan('command:slow')->assertExitCode(0);
}
False Positives:
sleep() or external APIs may legitimately be slow.@skip or adjust thresholds:
/** @skip Slow due to external API */
public function test_external_api() { ... }
CI Flakiness:
stderr output to debug:
<parameter name="stderr" value="true" type="bool"/>
PHPUnit Version Mismatch:
bootstrap vs. extension tag).Log Raw Durations: Add a custom listener to log all test durations (for debugging):
// tests/Listeners/DurationLogger.php
use PHPUnit\Runner\AfterLastTestHook;
use PHPUnit\Runner\TestListener;
class DurationLogger implements TestListener, AfterLastTestHook {
public function endTest(PHPUnit\Framework\Test $test, $time) {
file_put_contents(
'test_durations.log',
sprintf("%s: %.3f\n", $test->getName(), $time),
FILE_APPEND
);
}
}
Register in phpunit.xml:
<listeners>
<listener class="App\Tests\Listeners\DurationLogger"/>
</listeners>
Isolate Slow Tests: Run a single slow test with:
./vendor/bin/phpunit --filter=testPaymentProcessing
Custom Threshold Logic:
Extend the extension by implementing your own Ergebnis\PHPUnit\SlowTestDetector\Extension class:
use Ergebnis\PHPUnit\SlowTestDetector\Extension as BaseExtension;
class CustomExtension extends BaseExtension {
protected function getMaximumDuration(): int {
return (int) getenv('CUSTOM_THRESHOLD') ?: 500;
}
}
Register in phpunit.xml:
<extensions>
<bootstrap class="App\Tests\Extensions\CustomExtension"/>
</extensions>
Slack/Teams Notifications: Parse the slow test report and send alerts:
// scripts/send_slow_test_alert.php
$output = shell_exec('phpunit --configuration=phpunit-slow.xml 2>&1');
if (str_contains($output, 'Detected')) {
$message = "Slow tests detected!\n```$output```";
file_get_contents('https://hooks.slack.com/services/...', 'POST', [
'Content-Type' => 'application/json',
'data' => json_encode(['text' => $message])
]);
}
Avoid Global State: Slow tests often involve shared state (e.g., static caches). Use dependency injection:
// Bad: Static cache
public function test_slow_cache() {
Cache::remember('key', 3600, fn() => sleep(2));
}
// Good: Mock cache
public function test_fast_cache() {
$this->mock(Cache::class)->shouldReceive('remember')->andReturn('value');
}
Parallelize Independent Tests:
Use PHPUnit’s --parallel flag to offset slow test impact:
./vendor/bin/phpunit --parallel
Warm Up Dependencies: Initialize slow dependencies (e.g., database connections) outside tests:
// tests/bootstrap.php
DB::connection()->getPdo(); // Force connection early
How can I help you explore Laravel packages today?