phpunit/php-code-coverage
phpunit/php-code-coverage collects, processes, and renders PHP code coverage data. It provides drivers, filters, and report generation for PHPUnit test suites, including serialized coverage data and OpenClover output, helping teams track which lines of code tests execute.
Installation:
composer require --dev phpunit/php-code-coverage
Add to composer.json under require-dev if only needed for testing.
Basic Usage:
use SebastianBergmann\CodeCoverage\CodeCoverage;
use SebastianBergmann\CodeCoverage\Driver\Selector;
use SebastianBergmann\CodeCoverage\Filter;
use SebastianBergmann\CodeCoverage\Report\Facade;
$filter = new Filter;
$filter->includeFiles([__DIR__ . '/src/**/*.php']);
$coverage = new CodeCoverage(
(new Selector)->forLineCoverage($filter),
$filter
);
$coverage->start('Test Suite');
// Execute your tests or code here
$coverage->stop();
Facade::fromObject($coverage)->renderHtml(__DIR__ . '/coverage-report');
Key Classes:
CodeCoverage: Core class for collecting coverage data.Filter: Define which files/methods to include/exclude.Report\Facade: Generate reports (HTML, XML, etc.).Serialization\Serializer/Unserializer: Save/load coverage data.// In your test bootstrap file (e.g., tests/bootstrap.php)
$coverage = new CodeCoverage(
(new Selector)->forLineCoverage(),
new Filter
);
$coverage->start('Unit Tests');
// Run tests
$result = (new TestRunner)->run();
$coverage->stop();
Facade::fromObject($coverage)->renderCrap(__DIR__ . '/coverage-crap.txt');
Report\Facade for custom formats.Facade::fromObject($coverage)->render(
new CustomReportRenderer(),
__DIR__ . '/custom-report.json'
);
Filter to focus on critical paths.$filter = new Filter;
$filter->includeFiles([__DIR__ . '/src/Service/*.php'])
->excludeFiles([__DIR__ . '/src/Service/Logger.php']);
$serializer = new Serializer();
$serializer->serialize($coverage, __DIR__ . '/coverage.data');
// Later...
$unserializer = new Unserializer();
$data = $unserializer->unserialize(__DIR__ . '/coverage.data');
Facade::fromSerializedData($data)->renderHtml(__DIR__ . '/report');
$coverage = new CodeCoverage(
(new Selector)->forBranchCoverage($filter),
$filter
);
Service Provider Hook:
// In AppServiceProvider
public function boot()
{
if ($this->app->environment('testing')) {
$coverage = new CodeCoverage(
(new Selector)->forLineCoverage(),
new Filter
);
$coverage->start('Laravel Tests');
// Run tests via PHPUnit
$coverage->stop();
Facade::fromObject($coverage)->renderHtml(storage_path('coverage'));
}
}
Artisan Command:
// app/Console/Commands/GenerateCoverage.php
public function handle()
{
$coverage = new CodeCoverage(
(new Selector)->forLineCoverage(),
new Filter
);
$coverage->start('Artisan Command');
// Execute logic
$coverage->stop();
Facade::fromObject($coverage)->renderCrap(storage_path('coverage-crap.txt'));
}
Pest Integration:
// pest.php
beforeTests(function () {
$coverage = new CodeCoverage(
(new Selector)->forLineCoverage(),
new Filter
);
$coverage->start('Pest Tests');
});
afterTests(function () use ($coverage) {
$coverage->stop();
Facade::fromObject($coverage)->renderHtml(storage_path('pest-coverage'));
});
Driver Conflicts:
xdebug and pcov can cause invalid XML.Selector to explicitly choose:
(new Selector)->forLineCoverage()->withDriver('xdebug');
Path Coverage Overhead:
forPathCoverage()) significantly slows down tests.UTF-8 Validation:
Race Conditions:
--parallel) may cause race conditions in coverage data.CachingSourceAnalyser or serialize coverage data per test suite.Quadratic Merge Performance (fixed in 14.4.1+):
CodeCoverage::merge() copies all previously merged tests on every call, leading to quadratic time complexity when merging coverage from many processes (e.g., parallel test runs).phpunit/php-code-coverage 14.4.1 or later. If stuck on an older version, avoid calling merge() repeatedly; instead, serialize each process’s coverage and merge once at the end, or aggregate via a single process.Abstract Methods:
Attribute Lines:
#[Test]) are treated as executable.Dark Mode HTML Reports:
Verify Coverage Data:
$summary = Facade::fromObject($coverage)->summary();
$this->assertGreaterThan(80, $summary->getLineCoverageInPercent());
Inspect Filter Rules:
Filter::getIncludedFiles() and Filter::getExcludedFiles() to debug inclusion/exclusion logic.Check Driver Compatibility:
--debug flag in PHPUnit to see active drivers:
phpunit --debug
Validate XML Reports:
xmllint to validate generated XML:
xmllint --noout coverage.xml
Performance Bottlenecks:
Facade::fromObject($coverage)->renderHtml() to identify slow steps.merge() duration; upgrade to 14.4.1+ if you see quadratic growth.Custom Report Formats:
SebastianBergmann\CodeCoverage\Report\Renderer\RendererInterface:
class CustomRenderer implements RendererInterface {
public function render(CodeCoverage $coverage, string $path): void {
// Custom logic
}
}
Filter Extensions:
SebastianBergmann\CodeCoverage\Filter methods like isFileIncluded() for dynamic rules.Driver Plugins:
SebastianBergmann\CodeCoverage\Driver\DriverInterface for custom coverage drivers (e.g., for PHP 8.3+).Serialization Hooks:
SebastianBergmann\CodeCoverage\Serialization\Serializer to add custom metadata:
$serializer = new class($coverage) extends Serializer {
protected function getAdditionalData(): array {
return ['custom_key' => 'custom_value'];
}
};
HTML Report Customization:
SebastianBergmann\CodeCoverage\Report\Html namespace or extend the Renderer class.Storage Paths:
storage_path() for consistent coverage report storage:
Facade::fromObject($coverage)->renderHtml(storage_path('coverage'));
Artisan Commands:
if (!file_exists($coveragePath)) {
$coverage->stop();
Facade::fromObject($coverage)->renderHtml($coveragePath);
}
CI/CD Integration:
phpunit/php-code-coverage in combination with phpunit/phpunit for seamless CI integration:
# .github/workflows/coverage.yml
- name: Run tests with coverage
run: phpunit --coverage-clover=coverage.xml
--parallel), upgrade to phpunit/php-code-coverage 14.4.1+ to avoid quadratic merge() overhead when consolidating results.Pest Parallel Testing:
--parallel uses multiple processes. Ensure you merge coverage data efficiently:
// After all parallel runs, merge serialized coverage files
$coverage = new CodeCoverage(...);
foreach (glob(storage_path('coverage/*.data')) as $file) {
$coverage->merge((new Unserializer)->unserialize(file_get_contents($file)));
}
Facade::fromObject($coverage)->renderHtml(storage_path('coverage'));
How can I help you explore Laravel packages today?