What is Branch Coverage?
Line coverage tells you whether a line ran. Branch coverage tells you whether every possible outcome of a decision ran.
An if has two branches (true and false), a switch has one per case, and operators like &&, ||, ?? and the ternary add more.
function shippingCost(Order $order): int
{
if ($order->total > 100) {
return 0;
}
return 5;
}
Two tests, one with a total above 100 and one below, cover every line and both branches.
A single test with a total of 50 still runs the if line but never takes the true branch, so branch coverage is 50%.
On a one-liner like return $order->total > 100 ? 0 : 5; the difference is starker: one test gives you 100% line coverage and only 50% branch coverage.
That makes branch coverage the better signal for conditional logic, and much harder to game with tests that just run code without checking each path. For a deeper comparison, see Line Coverage vs Branch Coverage.
How OtterWise Tracks Branch Coverage
There is nothing to turn on in OtterWise. If your coverage report contains branch data, we read it automatically on every upload and store branch coverage for each commit, file and, where the report has it, each line and method. The catch is that branch data has to be in the report in the first place, which depends on two things:
- The report format you upload.
- The coverage tool or driver that collected the coverage.
Line coverage stays the headline Code Coverage number, and minimum coverage status checks are based on it. Branch coverage is tracked next to it.
Supported Formats
| Format | Branch coverage | What we read |
|---|---|---|
| Clover XML | Yes, per file and per line | The conditionals and coveredconditionals metrics on each file, and truecount/falsecount on cond lines where the tool writes them (Istanbul). |
| Cobertura XML | Yes, per file, per line and per method | The condition-coverage on each line (coverage.py, Istanbul, gcovr). Without it (PHPUnit), the branch-rate on each class (file) and method. |
| LCOV | No | Line coverage only. Branch records (BRDA, BRF, BRH) are ignored. |
| Go coverprofile | No | Go's coverage tooling only measures statements. |
If you upload LCOV today and want branch coverage, switch your reporter to Clover or Cobertura. Most JavaScript and TypeScript tools can write all three.
Supported Tools
| Language | Tool / driver | Branch coverage |
|---|---|---|
| PHP | Xdebug (PHPUnit, Pest) | Yes, with path coverage enabled (see below) |
| PHP | OtterWise Raft | Yes |
| PHP | PCOV | No, PCOV only collects line coverage |
| JavaScript / TypeScript | Jest, Vitest, nyc, c8 | Yes, with the clover or cobertura reporter |
| Python | coverage.py, pytest-cov | Yes, with branch measurement enabled (Cobertura XML, per file and per line) |
| Go | go test -cover |
No |
Using something else? If it writes conditionals in Clover, or condition-coverage or a non-zero branches-valid in Cobertura, OtterWise picks it up.
PHP: PHPUnit and Pest
PHPUnit (and therefore Pest) only records branches when path coverage is enabled, and only Xdebug can collect it.
With PCOV, or with Xdebug but without path coverage, the report contains conditionals="0" and there is no branch coverage to track.
1. Enable path coverage
In your phpunit.xml:
<coverage pathCoverage="true">
<report>
<clover outputFile="build/logs/clover.xml"/>
</report>
</coverage>
Or on the command line:
vendor/bin/phpunit --path-coverage --coverage-clover=build/logs/clover.xml vendor/bin/pest --path-coverage --coverage-clover=build/logs/clover.xml
Prefer Cobertura? Swap --coverage-clover for --coverage-cobertura=build/logs/cobertura.xml to also get branch coverage per method. PHPUnit's Cobertura report has no branch counts per line or file, only rates, so folders show no branch coverage and lines are not marked as partly covered. Clover gives counts per file. The uploader finds both paths automatically.
2. Run with Xdebug in coverage mode
With GitHub Actions and shivammathur/setup-php:
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
coverage: xdebug
- name: Run Tests
run: vendor/bin/phpunit --path-coverage --coverage-clover=build/logs/clover.xml
env:
XDEBUG_MODE: coverage
Path coverage is slow
Xdebug with path coverage is noticeably slower than line coverage, especially on large suites. If that is too slow to run on every push, OtterWise Raft collects branch coverage on its runners without Xdebug.
JavaScript and TypeScript
Istanbul-based tools (Jest, Vitest, nyc, c8) always measure branches. You only need to output Clover or Cobertura instead of, or in addition to, LCOV.
Jest (jest.config.js):
module.exports = {
collectCoverage: true,
coverageReporters: ['clover', 'text'],
};
Vitest (vitest.config.ts):
export default defineConfig({
test: {
coverage: {
reporter: ['clover', 'text'],
},
},
});
nyc / c8:
nyc --reporter=clover mocha c8 --reporter=clover npm test
These write coverage/clover.xml by default, so point the uploader at it with --file coverage/clover.xml.
Without --file, the uploader prefers coverage/lcov.info if it exists, and you would lose the branch data.
Python
coverage.py measures branches when branch mode is on, and its XML report is Cobertura. Enable it in pyproject.toml:
[tool.coverage.run] branch = true
Or with pytest-cov:
pytest --cov=src --cov-branch --cov-report=xml
coverage.py does not report per-method data, so you get branch coverage per file and per line.
Troubleshooting
If branch coverage is missing or always zero, open your coverage report and check:
- Clover: the file-level
<metrics>elements should have aconditionalsvalue above 0. - Cobertura: the
<class>elements should have abranch-rate, and the root<coverage>a non-zerobranches-valid. - LCOV: not supported for branch coverage. Switch to Clover or Cobertura.
- PHP: make sure you run Xdebug (not PCOV) with
XDEBUG_MODE=coverage, and that path coverage is enabled. - Python: make sure
branch = trueor--cov-branchis set.
Still stuck? Reach out and include a snippet of your coverage report.