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.
Plugin class: 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.. Not included in default plugins.
Requirements
One of the following PHP extensions is required:
- PCOV — lightweight, fast, line coverage only.
- XDebug ≥ 3.0 with
coveragemode 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:
return new ApplicationConfig(
src: ['src'],
//...
plugins: [
new CodecovPlugin(
level: CoverageLevel::Line,
reports: [
new CloverReport(__DIR__ . '/clover.xml', 'MyProject'),
new CoberturaReport(__DIR__ . '/cobertura.xml'),
],
),
],
);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::Line
enum 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\TestTypecases or custom string identifiers. $reports- Report generators to run after all tests complete. Each element must implement the CoverageReport
\Testo\Codecov\Report\CoverageReportinterface.
CoverageLevel
Defines the depth of coverage analysis. Each successive level includes all data from the previous one.
enum CoverageLevelEach 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:
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 CoverageModeThe 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\CoverageDriverNotAvailableexception. Set by the--coverageCLI flag. Never- Coverage is fully disabled, zero overhead. Set by the
--no-coverageCLI 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:
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-validare populated at all levels (coverage, package, class).- Lines with branch points get
branch="true"andcondition-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:
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:
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:
#[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:
#[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:
#[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:
#[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:
#[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 { ... }
}Using #[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. on the same level is an error. Testo will throw an exception identifying the conflicting test. Different levels (e.g., #[CoversNothing]#[CoversNothing]Excludes a test from coverage statistics. on the parent class and #[Covers]#[Covers(string $classOrFunction, ?string $method = null)]Restricts which source code counts toward coverage for this test. on the child) are valid.
Metadata
Coverage data for each test is attached to the TestResult\Testo\Core\Context\TestResult metadata under the CoverageResult\Testo\Codecov\Result\CoverageResult key:
use Testo\Codecov\Result\CoverageResult;
$coverage = $testResult->getAttribute(CoverageResult::class);
// CoverageResult|nullThis 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?
It depends on the activation mode. By default, CoverageMode::IfAvailableenum CoverageModeControls whether coverage is collected.IfAvailableDefault. Coverage is collected if an extension is available and configured, otherwise silently skipped. is used — the plugin silently skips coverage collection and tests run without it. If you run with the --coverage flag, the mode switches to CoverageMode::Alwaysenum CoverageModeControls whether coverage is collected.AlwaysCoverage is mandatory. If no extension is installed, tests will fail with a CoverageDriverNotAvailable\Testo\Codecov\Exception\CoverageDriverNotAvailable exception. Set by the --coverage CLI flag., and tests will fail with a CoverageDriverNotAvailable\Testo\Codecov\Exception\CoverageDriverNotAvailable exception.