Branch Coverage

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:

  1. The report format you upload.
  2. 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 a conditionals value above 0.
  • Cobertura: the <class> elements should have a branch-rate, and the root <coverage> a non-zero branches-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 = true or --cov-branch is set.

Still stuck? Reach out and include a snippet of your coverage report.