Skip to content

Test coverage

composer require quillstack/test-coverage

A library to create test coverage reports in PHP.

What quillstack/unit-tests uses to say how much of a package its tests actually run, and to write the report SonarCloud reads. It needs no extension: coverage comes from phpdbg, which ships with PHP.

Requirements

  • PHP 8.1 or newer
  • phpdbg, to measure anything — without it the tests still run, just without a number

Installation

shell
composer require --dev quillstack/test-coverage

Usage

php
use Quillstack\TestCoverage\CoverageOutput\CoverageXml;
use Quillstack\TestCoverage\Drivers\PHPDbg;
use Quillstack\TestCoverage\Drivers\NoCoverage;
use Quillstack\TestCoverage\TestCoverage;

$coverage = new TestCoverage(
    PHPDbg::isAvailable() ? new PHPDbg() : new NoCoverage(),
    new CoverageXml()
);

$coverage->start();
// … run the tests …
$coverage->end();

$xml = $coverage->process(__DIR__ . '/src', __DIR__);
$summary = $coverage->getSummary();
// ['covered' => 54, 'total' => 61, 'percent' => 88.5, 'files' => 4]

Run it under phpdbg, which is a separate binary shipped with PHP:

shell
phpdbg -qrr vendor/bin/unit-tests

A file nothing loaded still counts

phpdbg knows nothing about a file which was never compiled, so a class no test ever touched would drop out of the report altogether — and the percentage would come out better than the truth. Every file under the directory is compiled first, which puts its lines back into the total as uncovered.

It is not a small difference: it took one package in this stack from a reported 94.6% to an honest 80.3%.

Technical documentation

ClassWhat it is
TestCoveragethe way in: starts, stops, processes, and summarises
Drivers\PHPDbgmeasures with phpdbg
Drivers\NoCoveragemeasures nothing, so tests still run where phpdbg is not there
CoverageOutput\CoverageXmlwrites the report Sonar and friends read

TestCoverage implements TestCoverageInterface:

MethodDoes
isAvailable(): boolwhether anything can be measured at all
start(): void / end(): voidbegin and end collecting
process(string $dir, string $rootDir = ''): stringthe report, as a string
getSummary(): arraycovered, total, percent, files of the last run

$rootDir is taken off the front of every path, because the report is read somewhere other than the machine which wrote it and an absolute path from a CI runner matches nothing.

A driver implements TestCoverageDriverInterface (isAvailable(), start(), end(), process()), and an output implements TestCoverageOutputInterface (generate()) — so another way of measuring, or another format, is one class either way.

Unit tests

shell
composer test
composer test:coverage
composer stan

The driver is exercised in a process of its own, because phpdbg collects into one log at a time and starting a second inside this suite would take its own measurement away from it. That work therefore does not show up in this package's own percentage: the seven lines it reports as uncovered are the ones only a live log reaches, and they are tested out there.

License

MIT. See LICENSE.

Released under the MIT License.