Branch and Decision Coverage
Decision coverage measures how many outcomes of the decisions in the code your tests have exercised: both sides of every if, every case of a switch, and the "run again" and "leave" outcomes of a loop. Branch coverage, which the ISTQB Foundation syllabus v4.0 teaches instead, measures the branches of the control flow and in everyday code asks for the same tests.
Example: if (member) { discountPercent = 20; } has two outcomes. One test with a member runs every statement, but the non-member outcome, where nothing inside the if runs, is never tried. Both criteria require a second test for it.
Why it matters
Statement coverage, the subject of the previous lesson, cannot see an outcome that skips code: once the statements inside an if have run, it never asks what happens when they are skipped. Many defects live exactly there: a wrong default kept when the if is not taken, a missing else, an input that a guard such as if (qty > 0) should filter out but no test ever sends. Branch coverage forces a test through each of these outcomes.
For a tester, each uncovered outcome is a concrete question: what should happen for a non-member, an empty cart, an unknown status? The answer becomes a test with an expected result from the requirement, or reveals a defect or a missing requirement.
Decisions and their outcomes
A decision is a point where the code chooses which statement runs next; each possible choice is a decision outcome. Decision coverage is the decision outcomes exercised divided by all decision outcomes, as a percentage. Start by finding every decision; some are easy to overlook:
if, with or withoutelse: two outcomes. Without anelse, the false outcome skips the block and has no statement of its own.- The conditional operator
a ? b : c: two outcomes inside one statement, so statement and line coverage count it as covered even if one side never ran. - Loops (
for,while): enter the body or leave. A test that runs the body once takes both outcomes, so zero iterations are never required. switch: one outcome percase, plus the default outcome, even when nodefaultclause is written.if (a && b)is one decision with two outcomes; the conditions inside it are the subject of the next lesson.- Dense or minified code hides decisions: format it first so that each statement, including each
ifand loop, sits on its own line, then look inside expressions for?:.
Branch coverage and how it relates to decision coverage
The ISTQB Foundation syllabus v4.0 teaches branch testing. A branch is a transfer of control between two nodes of the control flow graph, the diagram of the orders in which statements can run, whose nodes are statements or blocks of statements: unconditional for straight-line code, conditional when it is a decision outcome. Branch coverage is the branches exercised divided by all branches, as a percentage.
Because the two criteria count different things, their percentages can differ below 100%, but in ordinary code the same tests reach 100% on both: once every decision outcome has run, every block after it has run too. Many teams use the names as synonyms; syllabus v3.1 taught decision coverage, and v4.0 replaced it with branch coverage.
- Coverage tools report "branches" and mostly mean decision outcomes; coverage.py measures them when run with
--branch. - Tools differ in what they count: some also count each operand of
&&and||, or each jump in compiled code. Compare percentages only from the same tool.
Why 100% branch coverage includes 100% statement coverage
Every executable statement is the end of at least one branch: control reaches it from the entry of the function, from the statement before it, or through a decision outcome. If your tests have taken every branch, control has arrived at every statement, so every statement has run. That is why the syllabus says branch coverage subsumes statement coverage: any test set reaching 100% branch coverage also reaches 100% statement coverage.
The reverse does not hold. The false outcome of an if without else contains no statement, so tests can run every statement without ever taking it.
- Subsumption is about 100% only. Below that, either percentage can be the higher one: a test that takes only the
trueside of anifwithoutelsecan score 100% of the statements and 50% of the decision outcomes, while a test that takes only the short side of anif/elsewhose other side is long scores 50% of the decision outcomes and a much smaller share of the statements. - Stronger is not sufficient: 100% branch coverage says nothing about combinations of outcomes, data-dependent defects such as a zero divisor, or code that was never written.
Worked example: a discount nobody asked for
Requirement: "Members get 20% off the price. Non-members pay the full price. Orders under 200.00 SAR after the discount pay 25.00 SAR shipping; orders of 200.00 SAR or more ship free." The code below has 8 executable statements and two decisions, on lines 3 and 8: 4 decision outcomes.
- T1
checkout(100, true)expects 80 + 25 = 105 and passes. It runs all 8 statements (100% statement coverage) but takes only the true outcome of each decision: 2 of 4, 50% decision coverage. - Missing: line 3 false (a non-member) and line 8 false (a total of 200.00 or more). One test takes both: T2
checkout(300, false), expecting 300. - T2 fails: the code returns 270, because line 2 starts every customer at 10% off, a default the requirement never mentions. Non-members get a discount they are not entitled to, and some of their orders just above 200.00 lose free shipping.
- With T1 and T2, decision coverage is 100%. A team that stopped at 100% statement coverage after T1 would have released the defect.
- Any value on each side satisfies the criterion; boundary value analysis adds totals of 199.99 and 200.00 to check the
<. After the fix, rerun them all as confirmation testing.
1 function checkout(price: number, member: boolean): number {
2 let discountPercent = 10;
3 if (member) {
4 discountPercent = 20;
5 }
6 const total = price - (price * discountPercent) / 100;
7 let shipping = 0;
8 if (total < 200) {
9 shipping = 25;
10 }
11 return total + shipping;
12 }
Test Expected Actual Result Outcomes taken
T1 checkout(100, true) 105 105 passed line 3 true, line 8 true
T2 checkout(300, false) 300 270 failed line 3 false, line 8 false
Coverage Statements Decision outcomes Not taken
T1 8/8 100% 2/4 50% line 3 false, line 8 false
T1 + T2 8/8 100% 4/4 100% noneif (member)Decision 1: two outcomesAnswer:
true: line 4 sets 20% offQuestion 2: Line 8:if (total < 200)Decision 2: total 80 in T1Answer:
true: line 9 adds 25 shippingOutcome: T1checkout(100, true)Expected 105, actual 105; alone it runs all 8 statementsPassAnswer:
false: line 9 skipped, free shippingOutcome: Member order of 200.00 or more after discountA path only: T1 and T2 already took each of its outcomesnot needed for 100%
Answer:
false: line 4 skipped, 10% staysQuestion 3: Line 8:if (total < 200)Decision 2: total 270 in T2Answer:
true: line 9 adds 25 shippingOutcome: Non-member order under 200.00A path only: T1 and T2 already took each of its outcomesnot needed for 100%Answer:
false: line 9 skipped, free shippingOutcome: T2checkout(300, false)Expected 300, actual 270: the 10% defaultFail
The dashed routes combine outcomes that T1 and T2 already took: they are further paths, which branch coverage does not require.
Try it: Boundary Value Generator Try it: Test Case Generator
Common mistakes
- Counting only outcomes that have code: the false side of an
ifwithoutelseis an outcome too, and the one statement coverage misses. - Reading 100% statement coverage as 100% branch coverage, or comparing branch percentages from tools that count branches differently.
- Reaching an outcome and then copying the expected result from the code instead of the requirement.
- Assuming 100% branch coverage means every combination, loop count or data value was tested: it means each outcome ran at least once.
- Forgetting implicit outcomes such as the default of a
switch, or assuming the empty-list case is covered because the loop exit ran.
The QA perspective
Branch coverage is usually measured on the developers' unit tests in CI. Ask for the branch figure of the changed code rather than the project total, and for its uncovered outcomes: each is a question about behaviour. Review new unit tests with the developer: does each one assert the result from the requirement, or only reach the line?
In reports, name the criterion and the tool: "pricing code, branch coverage (coverage.py --branch): 18 of 20 outcomes; the two missing are the non-member and empty-cart cases, tests added". The percentage is a floor for test design, not proof of quality.
Practice exercise
Reach 100% branch coverage for a cart total
Requirement: "The cart total is the sum of price × quantity of its lines. Lines with quantity 0 are ignored. The coupon WELCOME20 takes 20.00 SAR off the total; without a coupon the total is not reduced." Below are cartTotal and its only unit test, which passes.
Measure the existing test, counting decision outcomes as coverage tools do, then reach 100% decision coverage, and with it 100% branch coverage, with the fewest extra tests.
1 function cartTotal(items, coupon) {
2 let total = 0;
3 for (const item of items) {
4 if (item.qty > 0) {
5 total = total + item.price * item.qty;
6 }
7 }
8 let discount;
9 if (coupon === "WELCOME20") {
10 discount = 20;
11 }
12 return total - discount;
13 }
Unit test Expected Result
T1 cartTotal([{ price: 50, qty: 3 }], "WELCOME20") 130 passed- Find every decision, including the loop, and count the outcomes.
- Trace T1 and work out its statement and decision coverage.
- Design tests for the missing outcomes, with expected results from the requirement.
- Trace the new tests: do they pass? What does 100% still not require?
Show the model answer
Decisions: the loop on line 3 (enter or leave), the if on line 4 and the if on line 9: 6 outcomes.
T1 runs every statement: 100% statement coverage. With one cart line the loop enters once and then leaves, so T1 takes both loop outcomes, plus line 4 true and line 9 true: 4 of 6, 67% decision coverage.
Missing: line 4 false (a line with quantity 0) and line 9 false (no coupon). One test takes both: T2 cartTotal([{ price: 50, qty: 3 }, { price: 30, qty: 0 }], "") expects 150.
T2 fails: without a coupon discount is never assigned, and line 12 returns 150 - undefined, which is NaN. Report the input, expected 150, actual NaN and line 8; the fix is let discount = 0;. Statement coverage never asks for this test, because the false outcome of line 9 contains no statement.
Still not required: an empty cart (T1 already left the loop) and a coupon larger than the total. Add the empty cart from boundary value analysis, and ask the product owner about the large coupon.
Frequently asked questions
Are branch coverage and decision coverage the same thing?
Almost. Decision coverage counts decision outcomes; branch coverage, as ISTQB v4.0 defines it, counts every branch of the control flow graph, conditional and unconditional. In ordinary code the same tests reach 100% on both, and many teams and tools use the names interchangeably. Say which one you mean, and which tool measured it.
Does 100% branch coverage mean every path was tested?
No. A function with two independent if statements has four paths, and two tests can cover all four outcomes while running only two of the paths, as in the worked example. Covering combinations of outcomes and of conditions is the subject of the next lesson, on condition and path coverage.
What branch coverage percentage should a team aim for?
There is no universal number. Teams set a floor by risk: higher for pricing, security or safety logic, lower for simple glue code, and often on changed code only. Reviewing which outcomes are uncovered, and why, is worth more than a higher target. Never raise the number with tests that assert nothing.
References
- ISTQB Certified Tester Foundation Level (CTFL) v4.0The foundation syllabus of software testing: test levels and types, test design techniques, test management and tools.
- ISTQB GlossaryThe standard definitions of testing terms — test case, regression testing, severity, priority, boundary value analysis and the rest.
- NIST SP 500-235: Structured Testing: A Testing Methodology Using the Cyclomatic Complexity MetricStructured (basis path) testing: McCabe's cyclomatic complexity used to derive a basis set of independent paths to test — more thorough than statement and branch coverage.
Practise with Mutqan tools
Generate boundary values (min-1, min, min+1, nominal, max-1, max, max+1) for numeric and length constraints.
Generate structured test cases (id, title, preconditions, steps, expected result, priority) from a feature description and field list.
Beautify JavaScript and TypeScript code with consistent indentation and spacing.
Go deeper
Your progress on this topic
Not started
Mark the topic complete when you have studied it and done the exercise.
Sign in to track your progress
Mark topics complete, continue where you left off and unlock specialisation exams.