Code coverage with Testo
SkillDev toolsConfigure code coverage in Testo via CodecovPlugin, choose coverage level (Line/Branch/Path), wire up reports (Clover/Cobertura/PHPUnit XML), and use #[Covers] / #[CoversNothing] on tests. Use when the user asks about "code coverage", "clover", "cobertura", "infection coverage XML", or `#[Covers]`.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Code coverage with Testo skill
What this skill tells your AI
The instructions your AI receives, as published by php-testo/testo in skills/testo-coverage/SKILL.md and read by ahel’s review.
Coverage is opt-in via the CodecovPlugin in testo.php. The plugin needs:
- A coverage level —
Line,Branch, orPath(each adds cost and information). - One or more report writers — Clover, Cobertura, PHPUnit XML.
- Xdebug ≥ 3.1 (in coverage mode) or PCOV available on the runner. Without one of them, the plugin will skip. The coverage mode can be set via
xdebug.mode=coverage, the-d xdebug.mode=coverageCLI flag, or theXDEBUG_MODE=coverageenv var — Testo resolves the active mode withxdebug_info('mode'), so the env override (used bycomposer infectand IDE coverage runners) counts.
Fetch https://php-testo.github.io/llms.txt (and llms-full.txt if you need plugin wiring detail)
before editing — exact class names and constructor parameters are authoritative there.
Canonical wiring in testo.php
use Testo\Application\Config\ApplicationConfig;
use Testo\Codecov\CodecovPlugin;
use Testo\Codecov\Config\CoverageLevel;
use Testo\Codecov\Report\CloverReport;
use Testo\Codecov\Report\CoberturaReport;
use Testo\Codecov\Report\PhpUnitXmlReport;
return new ApplicationConfig(
src: ['src'],
suites: [/* ... */],
plugins: [
new CodecovPlugin(
level: CoverageLevel::Line,
reports: [
new CloverReport(__DIR__ . '/runtime/clover.xml', 'MyProject'),
new CoberturaReport(__DIR__ . '/runtime/cobertura.xml'),
new PhpUnitXmlReport(outputDir: __DIR__ . '/runtime/coverage-xml'),
],
),
],
);
Then enable on the CLI:
vendor/bin/testo --coverage
vendor/bin/testo --no-coverage # explicit off, overrides config
--coverage makes coverage mandatory (CoverageMode::Always): the run aborts with
CoverageDriverNotAvailable (non-zero exit) when no Xdebug/PCOV driver is present — even with no
report flags. That makes a bare vendor/bin/testo --coverage a handy CI gate to assert the driver
is actually available. --no-coverage always wins over everything.
CLI report flags (no testo.php needed)
A CodecovPlugin ships in the application defaults in shadow (inert) mode, so three flags let
external tools (the IDE plugin, Infection) pin report destinations without any testo.php change:
vendor/bin/testo --coverage-clover=build/clover.xml
vendor/bin/testo --coverage-cobertura=build/cobertura.xml
vendor/bin/testo --coverage-xml=build/coverage-xml # directory, for Infection
- Soft activation. Passing any of these implies coverage collection if a driver is available;
with no Xdebug/PCOV the run skips silently (no file).
--no-coveragestill wins and disables it. - Parallel with your config. If
testo.phpalready declares aCodecovPlugin, the flag-driven reports run alongside your configured ones — both sets of files are written. The two are merged into a single coverage collection (no double overhead): the deepest requested level wins, test-type filters are unioned, and every report (yours + the CLI ones) is emitted. - The shadow stays fully inert when no report flag is present, so default behavior is unchanged.
Choosing the level on the CLI
vendor/bin/testo --coverage-level=branch --coverage-clover=build/clover.xml
--coverage-level takes line, branch or path (case-insensitive) and pins the depth for the
whole run — it wins over every CodecovPlugin(level: …), including when it asks for less. An unknown
value aborts the run rather than falling back.
Without the flag the configured levels are merged and the deepest wins, so a
new CodecovPlugin(level: CoverageLevel::Branch) in testo.php still applies to a run that only the
CLI report flags activated.
Picking the coverage level
| Level | Cost | When |
|---|---|---|
Line | Low | Default for CI gates. |
Branch | Medium | When you need to be sure if/match/?: branches are exercised. |
Path | High | Mutation testing setup, exhaustive analysis. Usually local-only. |
Don't ship Path on every CI run — it's the slowest. Reserve it for mutation testing or scheduled jobs.
With fibers (#[RunInFiber]), use Line only. Branch and Path enable Xdebug's branch
analysis, which corrupts memory and crashes the process when it runs inside a fiber — no PHP error, no
report. The bug persists in current Xdebug builds (reproduced on 3.5.3). Testo tries to stop such a
test with BranchCoverageUnsafeInFiber, but the guard is tied to the Xdebug version and doesn't fire
on every build — don't rely on it. PCOV is unaffected (it only does Line anyway).
#[Covers] and #[CoversNothing]
Declare which production classes a test exercises. This scopes coverage reports and surfaces dead tests.
use Testo\Codecov\Covers;
use Testo\Codecov\CoversNothing;
#[Test]
#[Covers(UserService::class)] // class-level — applies to every test in the class
final class UserServiceTest { /* ... */ }
#[Test]
#[Covers(OrderTotal::class)]
#[Covers(TaxCalculator::class)] // repeatable: multiple covered targets
final class CheckoutTest { /* ... */ }
#[Test]
#[CoversNothing] // explicitly exclude from coverage attribution
final class SmokeTest { /* ... */ }
Rules (this is project policy in many Testo codebases — confirm before changing):
- Class-level
#[Covers]when every test in the class covers the same production class. This is the default. - Method-level
#[Covers]when tests in the same class cover different classes. - Free functions: pass the FQN as a string —
#[Covers('App\\helpers\\format_money')](verify againstllms.txtfor the version in use).
Reports cheat-sheet
| Report | Format id | Typical consumer |
|---|---|---|
CloverReport | clover | Codecov, Coveralls, GitHub coverage diffs. |
CoberturaReport | cobertura | GitLab/Jenkins coverage UI. |
PhpUnitXmlReport | coverage-xml | Infection (mutation testing). |
For Infection, point infection.json's coverage.path at the directory you gave to PhpUnitXmlReport.
Every written report is announced, not printed: the run dispatches
Testo\Event\Report\ReportFileGenerating once it knows coverage will be collected and
ReportFileGenerated after the file is written, and whichever renderer owns stdout states it — a plain
line in a terminal, a ##teamcity[testoReport …] service message under --teamcity. The format id in
the table is what a consumer switches on.
CoverageReport therefore has two methods: generate() and info(): ReportInfo
(Testo\Core\Report\ReportInfo — format, label, and a Stringable location). The location is what a
consumer opens: for a report that fills a directory, the index inside it rather than the directory; for
one that uploads its data, whatever URL it lands on.
final readonly class MyReport implements CoverageReport
{
public function __construct(private string $path) {}
public function generate(CoverageResult $result): void { /* write $this->path */ }
public function info(): ReportInfo
{
return new ReportInfo('my-format', 'My coverage', Path::create($this->path));
}
}
Pitfalls
- No coverage written? Check the active Xdebug mode includes
coverage— set it viaxdebug.mode,-d xdebug.mode=coverage, orXDEBUG_MODE=coverage(or load PCOV). Testo skips the driver if neither is available. clover.xmlempty? Suite-level finder probably excludes thesrcdirectory you expected — verify theFinderConfigcovers it.- Don't enable coverage in benchmark suites — it falsifies timings.
- Coverage under
#[RunInFiber]costs an extra driver stop/start per suspension (the window is closed around every fiber switch so each test keeps its own lines). Suspension-heavy tests pay for it — a reason to keep coverage runs onLineand off the interleaving-heavy suites. - Don't write
#[Covers(SomeInterface::class)]— point at concrete classes that own the executable code. - Don't combine
#[Covers]and#[CoversNothing]on the same class/method — pick one.
Signals
- GitHub stars
- 213
- Forks
- 16
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
testo-coverage- Source
- github.com/php-testo/testo