Branch Coverage
Did we exercise both sides of decisions? Better than line coverage. Path coverage is better still, but usually expensive and impractical at scale.
Branch coverage catches the case where a conditional’s true path is tested but the false path is not — a common gap that line coverage misses because the line containing the branch executes either way.
In practice
A branch coverage reading is a summary line per file, usually rendered by the same tool that measures line coverage, with two extra columns for decision points:
Name Stmts Miss Branch BrPart Cover
billing/invoice.py 214 31 88 14 79%
billing/tax.py 96 3 42 2 95%
TOTAL 310 34 130 16 83%
BrPart is the interesting number: branches that executed on one side
only. Reading it well:
- Read partial branches as a to-do list. Every entry in the BrPart column is a decision whose other side has never run. That is the specific, addressable gap, and it is what line coverage cannot show.
- Open the annotated source, not just the summary. Coverage tools mark the exact line where one side of a conditional never fired. The summary percentage hides this; the annotation is the actionable part.
- Treat low branch coverage on error paths as a finding. Untested
elsearms on validation and failure handling are where production surprises live, and they are the first place to spend new tests. - Do not chase 100%. The last few percent measure defensive code that cannot be reached without contrivance, and the tests written to reach it usually assert nothing.
How it gets gamed
Branch coverage counts execution, so it can be manufactured with execution:
- Assertion-free tests. A test that calls a function with inputs chosen to walk both sides of every conditional moves the branch number without verifying anything. The metric rises; the oracle does not. Mutation testing exposes this quickly: padded suites let most mutants through.
- Garbage inputs for defensive branches. Feeding malformed data into a parser exercises its error arms and counts as coverage, even though no test states what the parser should do with the input.
- Exclusion pragmas.
# pragma: no coverand equivalent annotations quietly remove inconvenient branches from the denominator. A growing exclusion list is coverage authority being spent, the same pattern as lint suppressions in a linter.
The meta-signal is the mutation score restricted to covered branches. If branch coverage climbs while it falls, the new tests are tourism.
Response playbook
When branch coverage drops or a report shows a gap worth closing:
- Find the uncovered branch in the annotated report. Open the source annotation and locate the decision whose missing side is flagged. Do not write a test from the percentage alone.
- Write the test for the missing side first. The untested arm is the specification: construct the input that takes it, and assert on the outcome, not merely that the code ran.
- Delete unreachable branches. If the missing arm is dead code, remove it. A branch that cannot execute is not coverage waiting to happen; it is clutter that dilutes the reading.
- Send back changes that drop coverage. If a drop is concentrated in one change, return it with the specific uncovered branches named. A blanket “add tests” instruction produces the padding above.
What it cannot detect
Same limitations as line coverage — execution is not assertion. Branch coverage tells you the branch ran, not that the right thing happened when it did.
Related sensors
References
Publications
Tooling
- coverage.pyPython code coverage measurement
- IstanbulJavaScript code coverage
- JaCoCoJava code coverage
- gcovGCC code coverage