← Test strategy

Practical guide

Branch Coverage: What It Measures and What It Misses

Branch coverage shows whether tests took both sides of a decision. Use it to find the paths that deserve a closer look, not to mistake a green percentage for proof.

Abstract branching execution paths in warm white and orange ending at a green verified point

The short version

Branch coverage measures decision outcomes. If code can take a true or false route, the metric asks whether a test took each route. That makes it more revealing than a simple count of executed lines around conditional logic. It still cannot tell you whether the test checked the outcome that matters, so read the test and its assertion before you treat the result as release evidence.

Coverage reports can make a test suite feel more complete than it is. A large percentage is neat, sortable and easy to put beside a pull request. But an engineer reviewing a change needs a more useful answer: when this condition is false, what happens, and would our test catch the wrong result?

Branch coverage is designed to bring that question into view. It looks at the forks in a program’s control flow: an if, a case, a loop condition or another decision that can send execution down different routes. The NIST definition of decision or branch coverage frames it as the percentage of branches evaluated both true and false by a test set. The detail matters because a line can run while a meaningful alternative never does.

What branch coverage actually counts

Consider a delivery rule that gives free shipping only when an order is at least $50. A test for a $75 order may execute the conditional line and confirm that shipping is free. That is useful, but it has not exercised the below-$50 outcome. If a future change accidentally makes every order free to ship, the one happy-path test may stay green.

Branch coverage treats those outcomes separately. It wants evidence that a test took the qualifying route and another took the non-qualifying route. A typical report counts the executed outcomes divided by all measurable outcomes. Tools differ in exactly what they instrument, especially around compound conditions and generated code, so teams should read their tool’s documentation before enforcing a threshold.

This is why branch coverage and decision coverage are often used as near-synonyms. Atlassian’s coverage documentation describes branch coverage as measuring which possible routes through flow-control structures were followed, including whether a Boolean expression evaluated both true and false. It is a measurement of execution, not a measurement of product intent.

Abstract conditional decision split into two execution routes, with one route shown as an unverified gap

Branch coverage vs. line coverage

Line coverage asks whether a source line executed. Branch coverage asks whether each outcome of a decision executed. They overlap, but they answer different review questions.

MetricUseful questionWhat it can hide
Line coverageWhich executable lines did a test run touch?A conditional line ran, but only one outcome was exercised.
Branch coverageWhich decision outcomes did a test run take?Both outcomes ran, but the assertion did not verify the right result.
Behavior reviewWould the test catch the regression a customer would notice?Nothing automatic, it requires engineering judgment.

A small example makes the difference clear. Imagine an if statement with a one-line action in its true path and more code after the condition. One test can take the true path and execute every line in that small function. The report may show 100% line coverage. Yet it has never shown what happens when the condition is false. Branch coverage exposes that missing decision outcome.

The reverse is not a license to ignore lines. An unexecuted line is still a useful lead. The best habit is to use both views, then relate them to the changed behavior in the pull request. The companion guide, Code Coverage vs. Test Coverage, explains why neither execution metric can substitute for a clear claim about the behavior under test.

Where branch coverage is most valuable

Branch coverage earns its keep where a decision changes the result a user, customer or downstream system receives. Authorization checks, payment rules, retry limits, validation failures, feature flags and fallback handling all contain choices that can hide expensive mistakes. A single success test is rarely enough when the decision has an important refusal, failure or alternate route.

Start with the change rather than the repository-wide number. If a pull request adds a permission check, ask which roles should pass and which should be denied. If it changes a retry policy, ask what happens before the final attempt, after the final attempt and when the dependency recovers. The code coverage analysis guide provides a five-pass review method for turning those questions into a useful review conversation.

Not every branch deserves identical effort. A debug-only choice and a decision that determines whether money moves are not equal risks. Google’s coverage guidance recommends using coverage to assess risk and find test gaps instead of chasing one universal number. That principle is especially helpful when a branch is defensive, unreachable in a realistic environment or already exercised at a different test level.

Four steps for reviewing an uncovered branch

  1. Name the decision in plain language. Describe the behavior, not only the file or method. “An expired invitation cannot create an account” is more useful than “exercise the else branch.”
  2. Identify the outcome that is missing. Read the coverage report beside the changed code. Is the gap a false condition, a default case, an error route or a loop exit? Be precise before you add a test.
  3. Choose the smallest realistic input. Create an input that makes the behavior occur naturally. A precise setup is more durable than a test that reaches the branch through unrelated state.
  4. Check what the assertion proves. Confirm that the test would fail if the decision were reversed, skipped or applied to the wrong value. A non-error response alone may not prove the permission, amount, saved record or emitted event is correct.

When a branch is intentionally excluded, write down why. The reason might be generated code, a platform constraint, a path guarded by an external guarantee or a case verified through a different layer. A clear exception is better than a mysterious gap, and it is better than a test added only to satisfy a dashboard.

Abstract engineering review board with orange execution paths and a single green verified endpoint

Common branch coverage traps

The first trap is treating every untested branch as equally urgent. An uncovered error message formatter may be low risk. An uncovered denial path in an access-control change is not. The report gives the same visual treatment to both, so the reviewer has to supply context. Prioritize decisions that affect money, permissions, data integrity, customer-visible state or recovery after failure.

The second trap is counting incidental execution as deliberate testing. An integration test may happen to pass through a branch on its way to another assertion. That can be valuable evidence, but it is weaker than a test whose setup and assertion make the branch’s expected outcome obvious. If the branch matters, make the test’s claim easy for the next reviewer to read.

The third trap is assuming a loop has only one interesting route. A test that runs a loop once can still miss the empty collection, the final iteration, the early exit and the error raised halfway through processing. The right test cases depend on the behavior, not on a blanket rule to hit every possible path. Path counts grow quickly in real software, which is why branch coverage is a practical review signal rather than a demand to enumerate every execution sequence.

Finally, avoid turning a threshold into a performance target. A fixed number can prevent a quiet decline, especially on changed code, but it can also reward artificial tests. Use a threshold as a prompt to inspect the diff, then leave room for an engineer to explain a meaningful exception. The goal is a stronger release decision, not a cleaner dashboard.

Set expectations that match the risk

There is no universal branch-coverage percentage that makes every change safe. A payment calculation, data deletion flow or access-control change should attract closer inspection than a cosmetic display rule. The important question is whether the untested outcome could create a costly or hard-to-detect failure, then whether the team has another credible source of evidence for it.

For an existing codebase, a changed-code target is often more helpful than a broad score. It keeps attention on the behavior entering production without demanding that a team repair every historical gap before they can ship a narrow improvement. Pair the target with a reviewable exception process: explain the gap, name the risk and state where the behavior is protected instead. That turns a percentage into a decision record.

Keep the report close to the pull request. Engineers should be able to move from an uncovered branch to the changed logic, then to the test that is meant to protect it. This short feedback loop is far more useful than a monthly scorecard because it makes the next action concrete while the intent behind the change is still fresh.

What 100% branch coverage still cannot prove

It cannot prove the right requirement was identified. A team can exercise every outcome of an incomplete rule and still miss the customer scenario that matters. It cannot prove the test has a strong oracle either. A test may take both routes, then assert only that the request did not throw an exception.

Compound conditions need extra care. For isAdmin && accountIsActive, a report may show the overall decision was true and false without demonstrating that each part of the condition was independently meaningful. High-assurance work may need a stronger criterion than ordinary branch coverage. For routine product work, the practical response is simpler: review the combinations that could alter the customer outcome, especially the ones the current change introduced.

Mutation testing can provide another challenge when the risk warrants it. Change a comparison, remove a condition or alter a return value in a controlled environment, then see whether a test fails. It is not necessary for every branch, but it helps answer the question coverage alone cannot: would this assertion notice a plausible defect?

Where TestMojo fits

TestMojo helps teams connect a code change to the behavior it may affect, the tests around that behavior and the evidence each test supplies. Branch coverage can point to an untraveled outcome. The next step is understanding whether that outcome is meaningful, which test claims to protect it and what result the test actually proves.

Use the free local behavior map to inspect those connections in a repository. Then compare the reasoning with the worked evidence example or explore the evidence workflow. Teams using AI to move test work faster can also see why an evidence-led AI test automation workflow keeps the review decision grounded in more than generated code and a passing run.

Frequently asked questions

What is branch coverage?

Branch coverage measures whether tests exercised each outcome of a control-flow decision. For a simple if statement, that generally means at least one test took the true outcome and at least one test took the false outcome.

Does 100% branch coverage mean a feature is fully tested?

No. It shows that every measured decision outcome ran, not that the test asserted the right result, covered every requirement or would detect the regression that matters. It is a strong prompt for review, not a release verdict on its own.

Is branch coverage better than line coverage?

Branch coverage is often a stronger signal around conditional logic because it asks whether both outcomes ran. Line coverage still helps locate code that did not execute. Neither measure replaces a review of the behavior, assertion and remaining risk.

How should a team use branch coverage in pull requests?

Use it to inspect changed decisions first. For each uncovered outcome, decide whether it represents meaningful behavior, an intentional exception, generated code or an unreachable path. Then record the reasoning instead of adding a low-value test merely to make the percentage green.