Re-run last failed test in PHPUnit

Take a look at the --filter cli option. You can find an example in the organisation docs and in the CLI Docs.

--filter

Only runs tests whose name matches the given pattern. The pattern can be either the name of a single test or a regular expression that matches multiple test names.

Assume your run phpunit Tests/ and Tests/Stuff/ThatOneTestClassAgain::testThisWorks fails:

your options are:

phpunit --filter ThatOneTestClassAgain

and

phpunit --filter testThisWorks

or most other strings that somehow make sense


Since PHPUnit 7.3, you can cache the results of your tests, then order your tests by defects.

In phpunit.xml, enable cacheResults:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit cacheResult="true"
         ...>

If you don't want to edit your phpunit.xml, you could also run your tests with the --cache-result flag.

When caching results, PHPUnit will create a .phpunit.result.cache file after running tests. Make sure to add this file to your global gitignore file.

You can run your tests like this to run previously failed tests first:

phpunit --order-by=defects --stop-on-failure

The way I've found to implement it is fairly easy, but requires logging to be implemented. You setup phpunit to log to a json file. Then you alter the phpunit command to something similar to:

cd /home/vagrant/tests && php -d auto_prepend_file=./tests-prepend.php /usr/local/bin/phpunit

What this does is auto_prepend a php file before the execution of phpunit. This way we can capture $argsv and supply the required filter command automatically to phpunit.

tests-prepend.php (make sure to amend the file pathway of the json log)

<?php

global $argv, $argc;
if(empty($argv) === false) {
    // are we re-running?
    $has_rerun = false;
    foreach ($argv as $key => $value) {
        if($value === '--rerun-failures') {
            $has_rerun = true;
            unset($argv[$key]);
            break;
        }
    }
    if($has_rerun === true) {
        // validate the path exists and if so then capture the json data.
        $path = realpath(dirname(__FILE__).'/../logs/report.json');
        if(is_file($path) === true) {
            // special consideration taken here as phpunit does not store the report as a json array.
            $data = json_decode('['.str_replace('}{'.PHP_EOL, '},{'.PHP_EOL, file_get_contents($path).']'), true);
            $failed = array();
            // capture the failures as well as errors but taking care not to capture skipped tests.
            foreach ($data as $event) {
                if($event['event'] === 'test') {
                    if($event['status'] === 'fail') {
                        $failed[] = array($event['test'], 'failed');
                    }
                    elseif($event['status'] === 'error' && $event['trace'][0]['function'] !== 'markTestIncomplete') {
                        $failed[] = array($event['test'], 'error\'d');
                    }
                }
            }
            if(empty($failed) === true) {
                echo 'There are no failed tests to re-run.'.PHP_EOL.PHP_EOL;
                exit;
            }
            else{
                echo '--------------------------------------------------------------------'.PHP_EOL;
                echo 'Re-running the following tests: '.PHP_EOL;
                foreach ($failed as $key => $test_data) {
                    echo ' - '.$test_data[0].' ('.$test_data[1].')'.PHP_EOL;
                    // important to escapre the namespace backslashes.
                    $failed[$key] = addslashes($test_data[0]);
                }
                echo '--------------------------------------------------------------------'.PHP_EOL.PHP_EOL;
            }
            $argv[] = '--filter';
            $argv[] = '/('.implode('|', $failed).')/';
            // important to update the globals in every location.
            $_SERVER['argv'] = $GLOBALS['_SERVER']['argv'] = $GLOBALS['argv'] = $argv = array_values($argv);
            $_SERVER['argc'] = $GLOBALS['_SERVER']['argc'] = $GLOBALS['argc'] = $argc = count($argv);
        }
        else{
            echo 'The last run report log at '.$path.' does not exist so it is not possible to re-run the failed tests. Please re-run the test suite without the --rerun-failures command.'.PHP_EOL.PHP_EOL;
            exit;
        }
    }
}