Open
Description
protected function runTest()
in PHPUnit\Framework\TestCase
provided an extension point to change the actual run implementation. That enabled various use-cases around exception handling and specialized test execution.
One additional use-case that has not been mentioned yet is to retry tests that might be unstable.
It would be terrific if PHPUnit could provide an officially blessed extension point to change test execution.
See related issues:
- Support for decorating
runTest
function #6172 - Proposal: Accessing Thrown Exceptions #5900
- PhpUnit 10 - Make exception handling extensible again #5630
Here is an example of how runTest
was used for a retry implementation:
/**
* @mixin TestCase
* @phpstan-require-extends TestCase
*/
trait ResilientTest
{
#[Override]
final protected function runTest(): mixed
{
if (!self::shouldRetryTest($this)) {
return parent::runTest();
}
$maxRetries = self::retryTimes($this);
for ($retry = 0; $retry < $maxRetries; ++$retry) {
try {
return parent::runTest();
} catch (SkippedTest|IncompleteTest $e) {
throw $e;
} catch (Throwable $e) {
if ($retry === $maxRetries - 1) {
throw $e;
}
error_log(sprintf('Retrying %s (%d of %d)', $this->name(), $retry + 1, $maxRetries));
}
}
return parent::runTest();
}
private static function shouldRetryTest(TestCase $testCase): bool
{
return self::retryTimes($testCase) > 1;
}
private static function retryTimes(TestCase $testCase): int
{
$class = new ReflectionObject($testCase);
$currentClass = $class;
do {
$classAttributes = $currentClass->getAttributes(Retry::class);
if (count($classAttributes) > 0) {
return $classAttributes[0]->newInstance()->times ?? 1;
}
$methodAttributes = $currentClass->getMethod($testCase->name())
->getAttributes(Retry::class);
if (count($methodAttributes) > 0) {
return $methodAttributes[0]->newInstance()->times ?? 1;
}
} while (($currentClass = $currentClass->getParentClass()) !== false);
return 1;
}
}