Skip to content

Extension point for single test execution #6209

Open
@lstrojny

Description

@lstrojny

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:

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;
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    type/enhancementA new idea that should be implemented

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions