testo/retry
Testo Retry plugin: automatically rerun failed tests using a configurable retry policy. Helps reduce CI noise from flaky, transient failures and keep pipelines stable while issues are investigated. Installed as a dev dependency via composer require --dev testo/retry.
Since testo/retry is Testo-specific, Laravel developers using Pest/PHPUnit should first evaluate alternatives (e.g., Pest plugins or custom traits). However, if you’re already using Testo, follow these steps:
Install Testo and the Retry Plugin:
composer require --dev php-testo/testo testo/retry
Enable the Retry Plugin in testo.php:
return [
'plugins' => [
Testo\Retry\RetryPlugin::class,
],
'retry' => [
'enabled' => true,
'policy' => 'exponential', // or 'fixed'
'max_attempts' => 3,
'delay' => 100, // milliseconds
],
];
Run Tests with Retries:
vendor/bin/testo
For a Laravel/Pest project, if you still want to leverage this package:
RetryPolicy class) and adapt it for Pest/PHPUnit:
// Example: Create a Pest-compatible retry trait
use Testo\Retry\RetryPolicy;
trait PestRetryTrait {
protected function retryTest(callable $test, int $maxAttempts = 3): void {
$policy = new RetryPolicy('fixed', $maxAttempts, 100);
$policy->execute($test);
}
}
Configure Retry Policies:
'retry' => [
'policy' => 'fixed',
'delay' => 100,
'max_attempts' => 3,
],
'retry' => [
'policy' => 'exponential',
'delay' => 100,
'max_attempts' => 3,
],
Tag Tests for Retry (if supported):
#[Test]
#[Retry] // Hypothetical tag (check Testo docs)
public function flaky_api_test(): void {
// Test code that may fail transiently
}
Global vs. Per-Test Retries:
Extract Core Logic:
Copy the RetryPolicy class from testo/retry into your project and modify it to work with Pest:
// app/Traits/RetryTrait.php
use Testo\Retry\RetryPolicy;
trait RetryTrait {
protected function retry(callable $callback, int $maxAttempts = 3): mixed {
$policy = new RetryPolicy('exponential', $maxAttempts, 100);
return $policy->execute($callback);
}
}
Use in Pest Tests:
use App\Traits\RetryTrait;
it('retries on failure', function () {
$this->retry(function () {
// Flaky test logic
if (rand(0, 1)) {
throw new RuntimeException('Transient failure');
}
return true;
});
});
Customize Policies:
Extend the RetryPolicy class to add new strategies (e.g., jitter, custom conditions):
class CustomRetryPolicy extends RetryPolicy {
public function shouldRetry(Throwable $exception): bool {
return $exception instanceof NetworkTimeoutException;
}
}
'retry' => [
'logger' => fn(string $message) => error_log($message),
],
False Positives:
Infinite Retries:
max_attempts: -1) can cause tests to loop forever.max_attempts and use a circuit breaker for known flakes.CI Timeouts:
delay or max_attempts for critical paths.Environment Mismatches:
Testo-Specific Quirks:
'retry' => [
'logger' => fn(string $message) => Logger::info($message),
],
vendor/bin/testo --filter="flaky_test"
shouldRetry() in a custom policy to filter exceptions:
class CustomPolicy extends RetryPolicy {
public function shouldRetry(Throwable $e): bool {
return $e instanceof SocketException && $this->attempt < 3;
}
}
Custom Policies:
Extend Testo\Retry\RetryPolicy to add new strategies:
class JitterPolicy extends RetryPolicy {
protected function getDelay(): int {
return parent::getDelay() + rand(0, 50); // Add jitter
}
}
Pre/Post-Retry Hooks: Add callbacks before/after retries:
'retry' => [
'hooks' => [
'before' => fn() => Logger::info('Retrying test...'),
'after' => fn(Throwable $e) => Logger::error('Test failed after retries', ['exception' => $e]),
],
],
Testo Plugin Integration: If using Testo, explore its event system to trigger retries on specific test events:
Testo::on(TestFailed::class, function ($event) {
if ($event->test->hasTag('retryable')) {
$event->retry();
}
});
testo/retry logic:
// pest.php
plugins([
new class implements Pest\Plugin {
public function process(TestCase $testCase): void {
$testCase->retry(fn() => $testCase->run());
}
},
]);
// app/Providers/AppServiceProvider.php
use Testo\Retry\RetryPolicy;
public function boot(): void {
if (!app()->runningInConsole()) {
app()->singleton('retry', fn() => new RetryPolicy('exponential', 3, 100));
}
}
How can I help you explore Laravel packages today?