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 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.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.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
**Pest
How can I help you explore Laravel packages today?