In CI, how do you make PHPUnit 13 produce HTML and Clover coverage reports, and how do you choose between PCOV and Xdebug as the driver?
answer
- PHP ships no coverage driver
- <source> is the filter
- --coverage-html dir, --coverage-clover file
- PCOV: line coverage only, low overhead
- branch and path coverage need Xdebug
basics
~20 sInstall a coverage driver - PCOV for fast line coverage, Xdebug in coverage mode for branch and path coverage - declare <source>, then pass --coverage-html and --coverage-clover or configure <coverage><report>. Without a driver PHPUnit warns and writes nothing.
solid answer
~40 sPHPUnit does not measure coverage itself; a PHP extension does. **PCOV** records line coverage with little overhead and nothing else; **Xdebug** must run in its coverage mode, is slower, and is the driver that supports branch and path coverage (`--branch-coverage`, `--path-coverage`). With neither loaded, a coverage run ends with the PHPUnit warning "No code coverage driver available that supports line coverage", which fails the run by default. Coverage also needs `<source>`, the filter of first-party files. Reports come from CLI options - `--coverage-html build/coverage` (a directory), `--coverage-clover build/logs/clover.xml` (a file), `--coverage-text`, `--coverage-cobertura` - or from `<coverage><report>` in `phpunit.xml`; `--no-coverage` ignores the configured reports. In CI I run coverage in one dedicated job with PCOV, keep other jobs driver-free for speed, and switch to Xdebug only when branch coverage is required.
code
xml · 22 lines<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
cacheDirectory=".phpunit.cache">
<testsuites>
<testsuite name="unit">
<directory>tests/Unit</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory>src</directory>
</include>
</source>
<coverage>
<report>
<html outputDirectory="build/coverage"/>
<clover outputFile="build/logs/clover.xml"/>
<text outputFile="php://stdout" showOnlySummary="true"/>
</report>
</coverage>
</phpunit>go deeper
Recall that coverage needs an extension such as PCOV or Xdebug, and that --coverage-html writes a report into a directory.
Explain the driver trade-off, the role of <source>, the report options, and the warnings PHPUnit gives when a piece is missing.
Design the CI layout: driver-free test jobs, one coverage job with PCOV or Xdebug, Clover for dashboards, HTML as an artifact, failing on missing coverage.
Decide whether branch coverage is worth Xdebug's cost for the whole organisation, and how coverage reports feed review without becoming a vanity target.
## Who measures coverage PHPUnit 13 turns coverage data into reports, but it cannot observe which lines executed on its own. A PHP extension - the **coverage driver** - does that, and neither candidate is bundled with PHP: - **PCOV** is a small PECL extension built only for line coverage. It adds little overhead and has nothing else to configure for PHPUnit. - **Xdebug** is a debugger and profiler that can also collect coverage when it runs in its coverage mode. It costs more per test but can also record **branch** and **path** coverage. How Xdebug's mode is set is Xdebug's own configuration topic. The `Runtime:` line in the run header shows the PHP version followed by `with` and the driver's name and version. Load exactly one of them in the coverage job so there is no doubt which one produced the numbers. ## What happens when something is missing | Missing piece | What PHPUnit does | |---|---| | No driver loaded | PHPUnit warning "No code coverage driver available that supports line coverage"; no report | | No `<source>` | PHPUnit warning "No filter is configured, code coverage will not be processed" | | `<source>` matches no files | PHPUnit warning naming the include paths; coverage skipped | | `--path-coverage` with PCOV | coverage cannot be initialised with that granularity; PHPUnit warns | Because `failOnPhpunitWarning` defaults to true, each of these warnings makes the run exit 1 even if every test passed - which is useful, since a coverage job that silently produces no report is worse than a red one. ## Asking for reports From the command line: - `--coverage-html <dir>` writes a browsable HTML site into a directory. - `--coverage-clover <file>` writes a Clover XML file, the format many CI dashboards and code-review tools read; `--coverage-openclover` and `--coverage-cobertura` write the other common XML formats. - `--coverage-text` prints a summary to standard output; `--only-summary-for-coverage-text` keeps it short. - `--branch-coverage` and `--path-coverage` add those granularities, with Xdebug. Or permanently in `phpunit.xml`, inside a `<coverage><report>` element with `<html outputDirectory>`, `<clover outputFile>`, `<text outputFile>` and siblings. `--no-coverage` ignores that configured reporting for a quick local run. ## A CI layout for a legacy project 1. **Test jobs** run without any coverage driver: fastest possible feedback. 2. **One coverage job** installs PCOV, runs the unit suite with `--coverage-clover` for the dashboard and `--coverage-html` as a downloadable artifact. 3. `<source>` points at `src/` only, with generated or third-party code excluded; the coverage element's `includeUncoveredFiles` default (true) lists never-loaded files at 0 %, so untested legacy code is visible instead of invisible. 4. If the team decides branch coverage matters, that job switches to Xdebug and adds `--branch-coverage`, accepting the slower run. 5. `--warm-coverage-cache` pre-fills the static-analysis cache PHPUnit uses for coverage, with `cacheDirectory` set, so repeated CI runs spend less time analysing unchanged source files. ## PCOV or Xdebug: the decision | Need | Driver | |---|---| | Line coverage for a dashboard, fast | PCOV | | Branch or path coverage | Xdebug in coverage mode | | Step debugging on the same machine anyway | Xdebug, but not left on in normal test jobs | A common trap is leaving Xdebug loaded in every CI job "for coverage": every test job pays its overhead even when no report is written. Keep the driver in the one job that needs it. ## Local runs Developers rarely need coverage on every local run. With reports configured in `phpunit.xml`, a plain `vendor/bin/phpunit` on a machine that has a driver loaded will collect coverage and write reports each time, which slows the feedback loop; `--no-coverage` turns that off for a quick check. Conversely, a developer without any driver gets the missing-driver warning as soon as the configuration asks for a report, which is a hint to either install PCOV or move report configuration out of the committed file and into the CI command line. ## Reading the result The numbers only mean something relative to the targeting and strictness rules the suite uses; coverage as a metric, and what it cannot tell you, is its own topic. For this leaf the practical point is operational: a driver, a `<source>` filter and an explicit report option are all required, and each missing piece is announced by a PHPUnit warning that fails the run.
- With PHPUnit 13, the coverage job is green but no clover.xml appears. What do you check first?The warnings at the end of the output. The usual causes are no driver loaded ('No code coverage driver available that supports line coverage'), no `<source>` element, or `<source>` paths that match no files. A green run with such a warning means `failOnPhpunitWarning` was switched off, which hides exactly this problem.
- In PHPUnit 13, why keep the coverage driver out of the normal CI test jobs?Collecting coverage adds per-test overhead, and an extension such as Xdebug enabled in a job adds overhead even when no report is requested. Test jobs exist for fast feedback, so they run without a driver; one dedicated job loads PCOV, or Xdebug when branch coverage is needed, and publishes the reports.
saying these in an interview costs you the question
- Believes PHPUnit measures coverage without any extension
- Thinks PCOV can report branch or path coverage
- Loads Xdebug in every CI job just in case coverage is needed
- Expects coverage to work without a <source> element
- Passes a file name to --coverage-html instead of a directory