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