Skip to content
...

Code Coverage

The plugin collects code coverage data during test execution and generates reports in standard formats. Reports can be used in CI services (Codecov.io, SonarQube, GitHub Actions) and in IDEs — for example, PhpStorm can display coverage directly in your code from a Clover report.

Requirements

One of the following PHP extensions is required:

  • PCOV — lightweight, fast, line coverage only.
  • XDebug ≥ 3.0 with coverage mode enabled (xdebug.mode=coverage).

When both extensions are available, Testo prefers PCOV due to its lower overhead. If neither extension is installed, behavior depends on the plugin's activation mode (CoverageModeenum CoverageModeControls whether coverage is collected.).

Setup

Register CodecovPluginnew CodecovPlugin(CoverageLevel $level = CoverageLevel::Line, CoverageMode $collect = CoverageMode::IfAvailable, array $testTypes = [TestType::Test, TestType::TestInline], array $reports = [])Configures code coverage collection: analysis depth, activation mode, and report formats. in the plugins section of your configuration:

php
return new ApplicationConfig(
    src: ['src'],
    //...
    plugins: [
        new CodecovPlugin(
            level: CoverageLevel::Line,
            reports: [
                new CloverReport(__DIR__ . '/clover.xml', 'MyProject'),
                new CoberturaReport(__DIR__ . '/cobertura.xml'),
            ],
        ),
    ],
);
php
return new ApplicationConfig(
    src: ['src'],
    suites: [
        new SuiteConfig(
            // ...
            plugins: [
                new CodecovPlugin(
                    reports: [
                        new CloverReport(__DIR__ . '/clover.xml', 'MyProject'),
                    ],
                ),
            ],
        ),
    ],
);

At the application level, coverage is collected across all test suites. At the test suite level — only for that specific suite. Reports are generated after tests complete. Coverage is filtered to files matching the src parameter from ApplicationConfig\Testo\Application\Config\ApplicationConfig.

CodecovPlugin

Configures code coverage collection: analysis depth, activation mode, and report formats.

new CodecovPlugin(CoverageLevel $level = CoverageLevel::Line, CoverageMode $collect = CoverageMode::IfAvailable, array $testTypes = [TestType::Test, TestType::TestInline], array $reports = [])

Parameters:

$level
Coverage analysis depth. Defaults to CoverageLevel::Lineenum CoverageLevelDefines the depth of coverage analysis. Each successive level includes all data from the previous one.LineWhich source lines were executed. Supported by both PCOV and XDebug..
$collect
Default activation mode. CLI flags (--coverage, --no-coverage) take priority over this value.
$testTypes
Test types to collect coverage for. Coverage collection adds overhead to each run, so benchmarks are excluded by default — otherwise performance measurements would be skewed. By default, coverage is collected only for regular tests and inline tests (TestType::Test\Testo\Core\Value\TestType::Test, TestType::TestInline\Testo\Core\Value\TestType::TestInline). An empty array means all types. Accepts TestType\Testo\Core\Value\TestType cases or custom string identifiers.
$reports
Report generators to run after all tests complete. Each element must implement the CoverageReport\Testo\Codecov\Report\CoverageReport interface.

CoverageLevel

Defines the depth of coverage analysis. Each successive level includes all data from the previous one.

enum CoverageLevel

Each successive level increases analysis overhead. PCOV only supports Line — when a deeper level is requested, it silently falls back to Line.

Example. Consider the following code:

php
function greet(bool $loud, bool $formal): string
{
    $greeting = $formal ? 'Good day' : 'Hi';         // 2 branches
    return $loud ? strtoupper($greeting) : $greeting; // 2 branches
}

A test calling greet(true, true):

  • Line — 100%: both lines executed.
  • Branch — 50%: 2 of 4 branches taken.
  • Path — 25%: 1 of 4 paths followed (true+true, true+false, false+true, false+false).

Cases:

Line
Which source lines were executed. Supported by both PCOV and XDebug.
Branch
Line + which branches (if/else, switch, ?:, ??) were taken. XDebug only.
Path
Branch + which complete execution paths through each function were followed. XDebug only.

CoverageMode

Controls whether coverage is collected.

enum CoverageMode

The default behavior is set by the collect parameter of the CodecovPluginnew CodecovPlugin(CoverageLevel $level = CoverageLevel::Line, CoverageMode $collect = CoverageMode::IfAvailable, array $testTypes = [TestType::Test, TestType::TestInline], array $reports = [])Configures code coverage collection: analysis depth, activation mode, and report formats. constructor, and CLI flags can override it at runtime. This means the plugin can safely remain in testo.php across all environments — on CI without PCOV/XDebug, tests will run normally, just without reports.

Cases:

IfAvailable
Default. Coverage is collected if an extension is available and configured, otherwise silently skipped.
Always
Coverage is mandatory. If no extension is installed, tests will fail with a CoverageDriverNotAvailable\Testo\Codecov\Exception\CoverageDriverNotAvailable exception. Set by the --coverage CLI flag.
Never
Coverage is fully disabled, zero overhead. Set by the --no-coverage CLI flag.

Reports

All built-in report generators implement the CoverageReport\Testo\Codecov\Report\CoverageReport interface. You can implement it to add your own output format.

CloverReport

Generates a Clover XML report.

new CloverReport(string $outputPath, string $projectName = '')

The format contains <file>, <line>, and <metrics> elements — line-level statement coverage only. Branch and path data is not included, as the format does not support it.

Compatible with: Codecov.io, SonarQube, Atlassian Clover.

Parameters:

$outputPath
Absolute path to the output XML file.
$projectName
Project name in the <project> element. Defaults to an empty string.

Examples:

php
new CloverReport(__DIR__ . '/clover.xml', 'MyProject')

CoberturaReport

Generates a Cobertura XML report.

new CoberturaReport(string $outputPath, string $sourceRoot = '')

Files are grouped into <package> elements by directory, with relative paths from sourceRoot.

When branch data is available (CoverageLevel::Branchenum CoverageLevelDefines the depth of coverage analysis. Each successive level includes all data from the previous one.BranchLine + which branches (if/else, switch, ?:, ??) were taken. XDebug only. or higher):

  • branch-rate, branches-covered, branches-valid are populated at all levels (coverage, package, class).
  • Lines with branch points get branch="true" and condition-coverage="50% (1/2)" attributes.

Without branch data, all branch attributes are 0.

Compatible with: GitHub Actions, GitLab CI, Jenkins.

Parameters:

$outputPath
Absolute path to the output XML file.
$sourceRoot
Source root for relative file paths. Defaults to getcwd().

Examples:

php
new CoberturaReport(__DIR__ . '/cobertura.xml')

Coverage Control

The src parameter in the ApplicationConfig\Testo\Application\Config\ApplicationConfig configuration defines the global set of files included in coverage. The #[Covers]#[Covers(string $classOrFunction, ?string $method = null)]Restricts which source code counts toward coverage for this test. and #[CoversNothing]#[CoversNothing]Excludes a test from coverage statistics. attributes allow fine-grained control over coverage for individual tests.

Global Filter

Includes and excludes are supported via FinderConfig\Testo\Application\Config\FinderConfig:

php
return new ApplicationConfig(
    src: new FinderConfig(
        include: ['src'],
        exclude: ['src/Generated'],
    ),
    // ...
);

Include only the directories you need in src to filter out unnecessary files before they are even loaded. This gives the best performance.

#[Covers]

Restricts which source code counts toward coverage for this test.

#[Covers(string $classOrFunction, ?string $method = null)]

Only lines belonging to the specified classes, traits, enums, methods, or functions will be included in the report. Everything else is discarded. The attribute is repeatable: multiple #[Covers]#[Covers(string $classOrFunction, ?string $method = null)]Restricts which source code counts toward coverage for this test. on the same test are combined.

When placed on a class, applies to all tests within it.

Parameters:

$classOrFunction
Fully qualified class, trait, enum (UserService::class, Cacheable::class, OrderStatus::class), or function name ('App\Helpers\format_name').
$method
Method name within the class, trait, or enum. When specified, only lines of that method are included, not the entire entity.

Examples:

Coverage for a class, trait, or enum — all executable lines:

php
#[Covers(UserService::class)]
public function testCreateUser(): void { ... }

#[Covers(Cacheable::class)]
public function testCacheableBehavior(): void { ... }

#[Covers(OrderStatus::class)]
public function testOrderStatusTransitions(): void { ... }

Coverage for a specific method — works with classes, traits, and enums:

php
#[Covers(UserService::class, 'create')]
public function testCreateUser(): void { ... }

#[Covers(Cacheable::class, 'invalidate')]
public function testCacheInvalidation(): void { ... }

#[Covers(OrderStatus::class, 'canTransitionTo')]
public function testStatusTransition(): void { ... }

Multiple targets — coverage is combined:

php
#[Covers(UserService::class)]
#[Covers(UserRepository::class, 'findById')]
public function testCreateUser(): void { ... }

#[CoversNothing]

Excludes a test from coverage statistics.

#[CoversNothing]

The test runs as usual, but the coverage driver is not started — zero overhead, no data is collected or included in reports. Useful for smoke tests and integration checks that touch a lot of code but shouldn't skew your coverage picture.

When placed on a class, applies to all tests within it.

Examples:

php
#[CoversNothing]
public function smokeTest(): void
{
    // Test runs, but coverage is not collected
    $response = $this->app->get('/health');
    Assert::same(200, $response->statusCode);
}

Attribute Priority

Coverage attributes are resolved layer by layer: the method is checked first, then the class. If the method has any coverage attribute, class-level attributes are ignored entirely. This allows overriding behavior in subclasses:

php
#[CoversNothing]
abstract class BaseIntegrationTest
{
    // By default, all tests in subclasses skip coverage
}

#[Covers(PaymentService::class)]
final class PaymentServiceTest extends BaseIntegrationTest
{
    // This subclass overrides the behavior — coverage is collected
    public function testCharge(): void { ... }
}

Metadata

Coverage data for each test is attached to the TestResult\Testo\Core\Context\TestResult metadata under the CoverageResult\Testo\Codecov\Result\CoverageResult key:

php
use Testo\Codecov\Result\CoverageResult;

$coverage = $testResult->getAttribute(CoverageResult::class);
// CoverageResult|null

This allows you to implement custom logic based on coverage data before results are reflected in the reports.

Which extension is better — PCOV or XDebug?

PCOV is faster and easier to set up, but only supports line coverage. XDebug is required for branch and path analysis. If CoverageLevel::Lineenum CoverageLevelDefines the depth of coverage analysis. Each successive level includes all data from the previous one.LineWhich source lines were executed. Supported by both PCOV and XDebug. is sufficient for your needs — use PCOV.

What happens if no coverage extension is installed?