phpunit/php-code-coverage
phpunit/php-code-coverage collects, processes, and renders PHP code coverage data. Integrate it in test runs to start/stop coverage collection, filter included files, and generate reports such as OpenClover, including from serialized coverage data.
Installation:
composer require --dev phpunit/php-code-coverage
Add to composer.json under require-dev to ensure it’s only installed for development.
Basic Usage with PHPUnit:
Configure PHPUnit’s phpunit.xml to generate coverage:
<phpunit>
<extensions>
<extension class="SebastianBergmann\CodeCoverage\CodeCoverage"/>
</extensions>
<coverage>
<include>
<directory>./src</directory>
</include>
<report>
<html>./coverage-report</html>
<clover>./coverage.clover</clover>
</report>
</coverage>
</phpunit>
Run tests with coverage:
./vendor/bin/phpunit
Manual Integration:
For custom workflows (e.g., CI pipelines), use the CodeCoverage class directly:
use SebastianBergmann\CodeCoverage\CodeCoverage;
use SebastianBergmann\CodeCoverage\Driver\Selector;
use SebastianBergmann\CodeCoverage\Filter;
$filter = new Filter();
$filter->includeFiles(['./src/**/*.php']);
$coverage = new CodeCoverage(
(new Selector())->forLineCoverage($filter),
$filter
);
$coverage->start('My Test Suite');
// Execute test code...
$coverage->stop();
$coverage->getReport()->render('html', './custom-report');
$filter = new Filter();
$filter->excludeFiles(['./tests/**', './vendor/**']);
$filter->includeFiles(['./src/**/*.php']);
Filter::includePath() or excludePath() for glob patterns (e.g., ./src/Modules/*).$report = $coverage->getReport();
$report->render('html', './report'); // HTML dashboard
$report->render('clover', './clover.xml'); // Clover XML
$report->render('cobertura', './cobertura.xml'); // Cobertura
./coverage/$(date +%Y-%m-%d)) for CI artifacts.$serializer = new \SebastianBergmann\CodeCoverage\Serialization\Serializer();
$serializer->serialize($coverage, './coverage.data');
$unserializer = new \SebastianBergmann\CodeCoverage\Serialization\Unserializer();
$data = $unserializer->unserialize('./coverage.data');
$report = \SebastianBergmann\CodeCoverage\Report\Facade::fromSerializedData($data);
$report->render('html', './restored-report');
PHPUnit\Framework\TestListener to hook into coverage events:
$coverage = new CodeCoverage(...);
$coverage->start('Custom Suite');
$listener = new class implements TestListener {
public function endTest(Test $test, $time): void {
if ($test instanceof MyTest) {
$coverage->stop();
}
}
};
$this->listeners[] = $listener;
Selector:
$coverage = new CodeCoverage(
(new Selector())->forBranchCoverage($filter), // or forPathCoverage()
$filter
);
Unserializer::unserialize() with relative paths (since v14.0.0) to merge coverage from parallel workers:
# Worker 1
./vendor/bin/phpunit --coverage-php=coverage1.data --workers=2
# Worker 2
./vendor/bin/phpunit --coverage-php=coverage2.data --workers=2
# Merge
php merge-coverage.php coverage1.data coverage2.data merged.data
Path Resolution:
Filter::setAbsolutePath() or ensure consistent getcwd().PHP Version Quirks:
Selector::forLineCoverage() explicitly to avoid conflicts.Attribute Lines:
#[...] (PHP 8 attributes) are not executable but may be marked as covered. Exclude them with:
$filter->excludeLinesByToken([T_ATTRIBUTE]);
Race Conditions:
phpunit --parallel) may cause CachingSourceAnalyser race conditions. Use --process-isolation or merge coverage post-execution.UTF-8 Validation:
find src -type f -exec iconv -f UTF-8 -t UTF-8 {} > /dev/null || echo "Invalid UTF-8 found"
Inspect Coverage Data:
foreach ($coverage->getData() as $file => $lines) {
echo "$file: " . count($lines) . " lines\n";
print_r($lines);
}
Check Driver:
echo $coverage->getDriver()->getName();
HTML Report Issues:
--force with phpunit to regenerate reports.Ctrl+F5).Serialization Errors:
Custom Report Formats:
Extend SebastianBergmann\CodeCoverage\Report\Renderer\RendererInterface to create new formats (e.g., JSON for APIs):
class JsonRenderer implements RendererInterface {
public function render(CodeCoverage $coverage, string $path): void {
file_put_contents($path, json_encode($coverage->getData()));
}
}
Filter Logic:
Override SebastianBergmann\CodeCoverage\Filter to implement custom include/exclude rules (e.g., exclude magic methods):
$filter = new class extends Filter {
public function apply(CodeCoverage $coverage): void {
parent::apply($coverage);
$coverage->removeLinesByToken([T_MAGIC_METHOD]);
}
};
CI Artifacts:
Use Report\Facade::summary() to extract metrics for CI badges:
$summary = \SebastianBergmann\CodeCoverage\Report\Facade::fromObject($coverage)->summary();
echo "Coverage: {$summary->getLineCoveragePercentage()}%";
Output to a file for CI tools (e.g., GitHub Actions):
echo "COVERAGE_PERCENTAGE=$(php get-coverage.php)" >> $GITHUB_ENV
Performance Tuning:
$coverage->disableSourceAnalysis();
if [ -f coverage.data ]; then
./vendor/bin/phpunit --coverage-php=coverage.data
else
./vendor/bin/phpunit --coverage-php=coverage.data --coverage-text
fi
Xdebug Settings:
Ensure xdebug.mode=coverage (PHP 8.2+) or xdebug.coverage_enable=1 is set in php.ini or .env:
XDEBUG_MODE=coverage
XDEBUG_OUTPUT_DIR=./coverage
**PCOV
How can I help you explore Laravel packages today?