Test Automation

Statement Coverage vs Branch Coverage: Differences With Worked Examples

Statement Coverage vs Branch Coverage: Differences With Worked Examples

A test can execute every statement in a function while leaving a decision's false exit untouched. A green statement-coverage result can therefore hide a missing test input. Even after both exits are covered, the tests may still fail to check an important result.

The practical difference between statement coverage and branch coverage is the obligation each counts: executed statements versus exercised control-flow alternatives. Use those obligations to select tests, then assess their assertions separately. The same inputs should support both calculations, so a higher percentage cannot conceal a change in scope.

Table of Contents

What the Two Metrics Count

Statement coverage asks which executable statements were exercised. For a declared scope, calculate the number of distinct covered statement obligations divided by the total statement obligations, multiplied by 100. Statement testing uses that inventory to find code that no selected test reaches.

Branch coverage asks which alternatives at the modeled decision points were exercised. Calculate the distinct covered branch obligations divided by the total branch obligations, multiplied by 100. Branch testing can reveal a missing alternative even when taking it would execute no additional statement.

Coverage.py's versioned 7.15.4 documentation gives a concrete example. Its function tests x on line 2, assigns y on line 3 when x is true, and returns y on line 4. Calling it with a true value executes the function's statements, but never takes the direct transition from line 2 to line 4. The documentation identifies that missing jump as a partial branch. An explicit else block is unnecessary for a false exit to exist.

That example describes documented behavior, not a coverage run performed here. It also exposes an oracle question: taking the false exit reaches a return whose variable was not assigned on that route. Adding an input is only part of the test design; the expected behavior for that input must also be specified.

Keep the measurement units precise. Source lines, executable statements and bytecode instructions are not interchangeable in every language or tool. The worked comparison below declares its own logical obligations rather than presenting hypothetical numbers as a coverage.py report.

Use One Control-Flow Model for Both Measures

Consider a fictional delivery-fee rule. The base fee is 10 currency units. Members pay 5, unless the order total is at least 100, when delivery is free for everyone. The threshold rule takes precedence over membership. Inputs are a Boolean membership flag and a nonnegative integer order total; validation of other inputs is outside this example.

This is an unexecuted test-design illustration, not runnable application code or observed instrumentation output. Its six executable logical statements have stable identifiers:

  • S1: Set the fee to 10, then continue to S2.
  • S2: Evaluate membership. If true, go to S3; if false, go directly to S4.
  • S3: Set the fee to 5, then continue to S4.
  • S4: Evaluate whether the total is at least 100. If true, go to S5; if false, go directly to S6.
  • S5: Set the fee to 0, then continue to S6.
  • S6: Return the fee.

For this model, the statement denominator is six. Each assignment, each evaluated if statement and the return count once. These identifiers represent logical statements, not physical source-line numbers. We do not count a function definition, entry or exit marker, test harness or comment. All six obligations are reachable, and none is excluded.

The branch denominator is four: S2 to S3, S2 to S4, S4 to S5, and S4 to S6. Only the outgoing alternatives of the two decision statements count. Unconditional transitions such as S1 to S2 are outside this branch denominator. There are no loops, exceptions or compound Boolean conditions in the model.

Fixing those rules makes the comparison reproducible. Changing the layout of a future implementation or choosing another tool must not silently redefine these denominators.

Calculate Coverage for the Same Inputs

Start with T1: a member whose total is 100. Its expected fee is 0 because the threshold overrides the member discount. Its planned route is S1, S2, S3, S4, S5, S6. It visits every statement, but takes only the true alternative at each decision.

Statement coverage is therefore 6/6 × 100 = 100%. Branch coverage is 2/4 × 100 = 50%. The missing alternatives are S2 to S4 and S4 to S6. They skip assignments rather than introducing new ones, which explains why statement coverage does not expose them.

Use the following cases against the same model. Fractions count distinct obligations; percentages are rounded to one decimal where necessary. All routes and outputs are expected design results, not observed test executions.

Check

Inputs: Member, Total

Expected Fee

Statement Coverage

Branch Coverage

T1

True, 100

0

6/6 = 100%

2/4 = 50%

T2

False, 50

10

4/6 = 66.7%

2/4 = 50%

T3

True, 50

5

5/6 = 83.3%

2/4 = 50%

T4

False, 100

0

5/6 = 83.3%

2/4 = 50%

T1 + T2

Both cases above

0 and 10, respectively

6/6 = 100%

4/4 = 100%

T2 follows S1, S2, S4, S6. It never sets the member fee or the free-delivery fee, so it covers four of six statement obligations. It takes the two false alternatives, covering two of four branches. The expected return of 10 comes directly from the base-fee requirement.

Together, T1 and T2 visit all six statements and all four branch alternatives. Combine their covered sets, rather than averaging their percentages. Each statement or branch counts once regardless of repeated visits. In this bounded graph, full branch coverage also reaches every modeled statement. That relationship does not authorize comparing different tools' counters or differently excluded scopes as though they were identical.

T3 and T4 each visit five statements and two branches. They are useful despite adding no branch coverage to the T1-plus-T2 suite, because they exercise combinations and outputs that suite never checks.

Full Branch Coverage Can Still Miss the Fee Defect

Suppose the member-fee assignment uses 8 instead of the required 5. An output assertion in T1 still sees 0: S5 overwrites the incorrect intermediate fee. T2 still sees 10 because its route skips the member assignment. Both assertions can pass without revealing the wrong discount.

This is a logical counterexample, not an executed mutation experiment. The original T1-plus-T2 design covers every modeled statement and branch, yet its final-output checks do not distinguish the correct member fee from the incorrect one.

T3 supplies the missing observation. A member with a total of 50 must receive a fee of 5, so the incorrect value of 8 becomes visible at the return. Write that literal expected value from the requirement, not by repeating the application's fee calculation inside the assertion. Otherwise the test and implementation could reproduce the same mistake.

T4 checks a nonmember at the threshold and expects 0. Together, the four individual cases cover both membership values combined with both threshold outcomes. Branch coverage of each decision alone did not require all those combinations.

The broader limit is important for white box test design: structural test coverage records where execution went. It does not establish that the requirement is correct, that the chosen assertion is relevant, or that the test would reject an incorrect result. A test that calls a function and checks nothing can still contribute execution coverage.

Choose the Next Test From the Missing Evidence

Use a missing-coverage report as a test-design prompt, then keep the behavioral oracle independent. A practical sequence is:

  1. Fix the code revision and measurement scope. Identify the tool, enabled metric and exclusions before interpreting a percentage.
  2. Locate the missing statement or branch destination. For T1 alone, name the false exits S2 to S4 and S4 to S6 rather than writing a vague request for more tests.
  3. Choose inputs that reach the missing obligation. T2 covers both missing exits here; a different graph may require separate inputs or setup.
  4. Specify the expected outcome from the requirement. T2 must return 10, while T3 must return 5. Reaching the route is a different check from verifying its result.
  5. Observe actual execution and coverage separately when an implementation exists. Compare the collected evidence with the planned route; do not label an inferred route as measured coverage.
  6. Add tests for important requirements and boundaries even when structural coverage no longer increases. Keep the reason for each additional case visible.

For the threshold rule, design totals of 99, 100 and 101, with membership chosen to verify the intended fee precedence. Those inputs probe the boundary around “at least 100.” The table's T1 and T4 already address equality, but neither demonstrates every nearby behavior. Negative totals or invalid membership values need an explicit validation requirement before their expected results can be asserted.

Manual testers can perform this reasoning with a control-flow sketch and an expected-results worksheet. A UI pass result alone, however, does not prove that a specific internal branch was exercised. Actual structural measurement needs suitable code-level observation. Until that exists, report the cases as designed coverage obligations, not achieved code coverage.

Read the Report Before Comparing Percentages

Check what a tool's headline percentage includes. Coverage.py's branch documentation describes recording source-and-destination line pairs, then comparing observed transitions with possible ones. In branch mode, its overall file percentage includes both statement and branch execution opportunities. A combined percentage is not the same quantity as the standalone branch fraction used in our table.

Exclusions also change the question being measured. Coverage.py documents that excluding one alternative can mean a conditional is no longer counted as a branch. That reporting change does not execute the excluded behavior or verify its correctness. Review the justification before accepting an improved percentage, especially when the excluded code represents a business alternative.

Other tools have different units. JaCoCo documents instruction coverage at the Java bytecode level and considers a source line executed when at least one assigned instruction executes. Its branch counter covers if and switch alternatives, while exception handling is not counted as branches under that counter definition. A green line or a full branch figure should therefore be read using that tool's documented semantics, not a universal rule that every condition or exception creates the same obligation.

The small model deliberately avoids compound conditions. If an implementation combines several Boolean conditions, exercising the overall decision both ways should not automatically be reported as checking every condition combination. Likewise, full branch coverage does not mean every possible execution path has been tested. Return to the requirement and the actual control flow when deciding what remains important.

For the next coverage review, ask for one missing transition and one independently specified expected result. Statement coverage is useful for finding unreached executable work; branch coverage adds visibility into unvisited alternatives. Keep both alongside assertion review and requirement-based cases, so a structurally complete suite still has to demonstrate the behavior that users need.

Related

More on Test Automation

Create traceable test cases, prepare release-specific test runs, and keep approved coverage current as the product changes.