Condition and Path Coverage
Condition coverage looks inside a decision. The decision account.active && (amount <= account.dailyLimit || otpOk) holds three atomic conditions, Boolean expressions with no && or || inside them. Branch coverage asks whether the whole decision was true and false; condition coverage asks whether each condition was. Path coverage looks at the whole function: has every route from entry to exit been run?
Both go beyond the statement and branch coverage of the previous lessons, and neither is practical at 100% everywhere. The skill is choosing the right subset for risky logic and saying which one you chose.
Why it matters
Two tests give a decision full branch coverage however many conditions it holds. Suppose a developer forgets the brackets: account.active && amount <= account.dailyLimit || otpOk. Because && binds more tightly than ||, a blocked account with a confirmed one-time password (OTP) now gets its transfer approved. If the branch-coverage tests are all true and all false, both versions give the same results and the defect passes.
Wrong operators, missing brackets and conditions with no effect are typical defects in compound decisions, often in the rules that decide money and access. Condition-level criteria pick a few combinations that check each condition's effect on the decision, which is where these defects show, without testing every combination.
Conditions, decisions and condition coverage
As in the previous lesson, a decision is a point where the code chooses between outcomes, such as an if or a loop condition. The conditions are the Boolean expressions inside it; in the ISTQB glossary, an atomic condition is one that contains no logical operator. The function below has one decision with three atomic conditions: A is account.active, B is amount <= account.dailyLimit and C is otpOk.
Condition coverage measures which condition outcomes, true and false for each atomic condition, the tests exercised. It does not include branch coverage. Two tests, an active account over its limit without OTP (A true, B false, C false) and a blocked account within its limit with OTP (A false, B true, C true), give 100% condition coverage, yet both transfers are rejected and APPROVED never runs.
- Condition/decision coverage (also called decision condition coverage) closes the gap: it requires every condition outcome and every decision outcome.
- Short-circuit evaluation (short-circuiting in the ISTQB glossary): in JavaScript, Java and C#,
&&and||stop once the result is known (as do Python'sandandor): when A is false, B and C are never evaluated. This lesson designs tests on the truth table, but at run time the blocked-account test above never evaluates B or C, so a tool that measures conditions would not count their true outcomes. Tools also differ: some count each operand as a branch, others only whole decisions or lines; check yours before reporting condition coverage.
1 function reviewTransfer(account: Account, amount: number, otpOk: boolean) {
2 if (account.active && (amount <= account.dailyLimit || otpOk)) {
3 return 'APPROVED';
4 }
5 return 'REJECTED';
6 }Worked example: from condition coverage to MC/DC
Requirement: "A transfer within the daily limit is approved if the account is active. Above the limit it needs a confirmed OTP. Transfers from blocked accounts are always rejected." Three conditions give 2 × 2 × 2 = 8 combinations: list them as a truth table, as in the lesson on business rules, with the expected decision from the requirement.
Modified condition/decision coverage (MC/DC) requires each condition to be shown to change the decision on its own. The usual way is an independence pair: two tests that differ only in that condition and give different decisions. To find B's pair, keep A and C fixed, flip B and look for a change in the decision: with A true and C false, rows 2 and 4 give true and false. The aviation standard DO-178C requires MC/DC for its most critical software level, Level A.
A && (B || C): rows 4 and 5 give full condition coverage, decision false in both; rows 1 and 8 give condition/decision coverage; the chosen MC/DC set is rows 2, 3, 4 and 7; multiple condition coverage needs all 8.| Row · A B C | Decision | Condition coverage2 tests | Condition/decision coverage2 tests | MC/DC4 tests | Multiple condition coverage8 tests |
|---|---|---|---|---|---|
1T T T | T | not in this test set | test | not in this test set | test |
2T T F | T | not in this test set | not in this test set | shows B (with 4) | test |
3T F T | T | not in this test set | not in this test set | shows A (with 7), C (with 4) | test |
4T F F | F | test | not in this test set | shows B (with 2), C (with 3) | test |
5F T T | F | test | not in this test set | not in this test set | test |
6F T F | F | not in this test set | not in this test set | not in this test set | test |
7F F T | F | not in this test set | not in this test set | shows A (with 3) | test |
8F F F | F | not in this test set | test | not in this test set | test |
- Combination this criterion tests
- Row 7, a blocked account over its limit with OTP: the test that exposes the missing brackets
A = account.active, B = amount <= account.dailyLimit, C = otpOk. T: true, F: false; a decision of T means the transfer is approved.
- Condition coverage: rows 4 and 5, with the decision false in both. Condition/decision coverage: rows 1 and 8.
- MC/DC: rows 2, 3, 4 and 7. B is shown by rows 2 and 4, C by 3 and 4, A by 3 and 7. That is 4 tests for 3 conditions: MC/DC always needs at least one test more than the number of conditions.
- A could also use rows 2 and 6. Choose 3 and 7: row 7, a blocked account over its limit with OTP, exposes the missing-brackets defect. When a criterion leaves a choice, choose by risk.
- Multiple condition coverage: all 8 rows. It doubles with each condition; 10 conditions mean 1,024 combinations.
Text version
Decision: A && (B || C)
A = account.active B = amount <= account.dailyLimit C = otpOk
Row A B C Decision MC/DC independence pairs
1 T T T T
2 T T F T B: rows 2 and 4
3 T F T T C: rows 3 and 4, A: rows 3 and 7
4 T F F F B: rows 2 and 4, C: rows 3 and 4
5 F T T F
6 F T F F
7 F F T F A: rows 3 and 7
8 F F F F
Condition coverage rows 4, 5 decision never T
Condition/decision coverage rows 1, 8
MC/DC rows 2, 3, 4, 7
Multiple condition coverage all 8 rowsPath coverage, and why loops make it explode
A path is one route through the code from entry to exit; path coverage asks whether the tests ran every route. Paths multiply: three if statements in a row have 8 paths, while two tests can cover all their branches.
Loops are far worse, because each number of iterations is a different path. In cartTotal, each item takes one of two routes, so a cart of n items has 2^n paths: 1,048,576 for a full cart of 20 and 2,097,151 in total. A loop without an upper limit has infinitely many. Some paths are also infeasible: their conditions contradict each other, so no input can run them.
- For loops, apply boundary values to the iteration count: zero, one, a typical number and the maximum (here 0, 1, 5 and 20 items), plus 21 where items are added, which the cart must refuse.
- Path thinking finds defects that need a sequence of routes, such as state left over from one iteration that breaks the next.
1 function cartTotal(items: Item[]): number { // a cart holds 0 to 20 items
2 let total = 0;
3 for (const item of items) {
4 if (item.onSale) {
5 total = total + item.salePrice;
6 } else {
7 total = total + item.price;
8 }
9 }
10 return total;
11 }
Items in cart Paths
0 1
1 2
2 4
3 8
20 1,048,576
0 to 20 2,097,151 in totalBasis paths and cyclomatic complexity
Basis path testing, the core of the structured testing method in NIST SP 500-235, keeps paths manageable. Cyclomatic complexity, v(G), is the maximum number of linearly independent paths through a function's control flow graph: v(G) = edges − nodes + 2, where the edges are the graph's branches. When every decision is two-way, this equals the number of decisions plus one. A basis set is v(G) paths from which every other path can be combined.
cartTotal has two decisions, the loop and the if, so v(G) = 3, and three tests form a basis set: an empty cart, one regular item and one sale item. A basis set guarantees full branch coverage, not every combination: it is a systematic minimum, not complete path coverage.
- Counting only the
if,reviewTransferhasv(G) = 2. NIST SP 500-235 also counts each short-circuit&&and||as a decision, because each one can skip the rest of the expression, which gives 4; ESLint'scomplexityrule counts them too. Tools can differ, so compare figures from the same tool only. - Treat a high
v(G)as a signal: the function needs many tests and is hard to read, so it deserves closer review or refactoring.
Common mistakes
- Reading 100% condition coverage as "every decision went both ways".
- Testing a compound decision such as
a && (b || c)only once true and once false, so a wrong operator or missing brackets pass. - Assuming a test exercised a condition that short-circuit evaluation skipped.
- Aiming for 100% path coverage in code with loops instead of basis paths and chosen iteration counts.
- Reporting impossible combinations or infeasible paths as gaps instead of recording them with the reason.
- Choosing MC/DC pairs mechanically instead of the pair that tests the riskiest combination.
The QA perspective
Use condition-level criteria for high-risk logic such as payments, permissions and eligibility. In reviews, ask the developer to walk you through the compound decisions. Put the truth table in the test design, mark impossible rows with the reason, and turn the chosen rows into data-driven test cases with expected results from the requirement.
In reports, name the criterion and scope: "transfer approval rule: MC/DC with 4 tests". Raise a high cyclomatic complexity or a decision with many conditions early: it shows how many tests the logic needs and may be a reason to simplify the code first.
Practice exercise
Design MC/DC tests for an age rule
Requirement: "People aged 18 or over can open an account. People aged 13 to 17 need a parent's consent. Nobody under 13 can open an account." Below are the developer's function and its two unit tests, which reach 100% branch coverage.
Design condition-level tests with concrete ages and find out what the unit tests missed.
1 function canOpenAccount(age: number, parentConsent: boolean): boolean {
2 if (age >= 18 || (parentConsent && age > 13)) {
3 return true;
4 }
5 return false;
6 }
Unit tests Expected Result
U1 canOpenAccount(25, true) true passed
U2 canOpenAccount(9, false) false passed- Name the atomic conditions on line 2 and write the truth table without impossible rows.
- Find an independence pair per condition and combine them into a minimal MC/DC set.
- Choose ages at the requirement's boundaries, with expected results from it.
- Trace the tests: which one fails, and what did U1 and U2 never check?
Show the model answer
A is age >= 18, B is parentConsent, C is age > 13; the decision is A || (B && C). A true with C false is impossible (every age of 18 or over is above 13), so 6 of the 8 combinations remain.
Writing each row as A B C: A is shown by TFT (true) and FFT (false), B by FTT and FFT, C by FTT and FTF. The minimal MC/DC set is TFT, FFT, FTT and FTF: four tests.
Ages: 18 without consent → allowed; 17 without consent → refused; 17 with consent → allowed; 12 with consent → refused. Boundary value analysis adds 13 with consent → allowed.
That test fails: 13 > 13 is false, so the code puts 13 in the same row as 12 and refuses an account the requirement allows. Report the off-by-one defect on line 2; the fix is age >= 13.
U1 and U2 reached 100% branch coverage, but age alone decided U1 and U2 had no consent, so the consent rule was never tested. MC/DC showed the missing combinations; boundary value analysis chose the value that exposed the defect.
Frequently asked questions
Does MC/DC include decision coverage?
Yes, for the decisions it is applied to: each independence pair shows the decision once true and once false. It says nothing about code outside those decisions, or about behaviour that was never implemented.
Which criterion should I use?
Use branch coverage as the baseline. For compound decisions in high-risk rules, add condition/decision coverage or MC/DC. Multiple condition coverage is affordable when a decision has two or three conditions. Full path coverage fits only small functions without loops; elsewhere, test a basis set and the important iteration counts.
Do I have to calculate cyclomatic complexity by hand?
Rarely. Static analysis tools and linters calculate it per function; ESLint's complexity rule, for example, flags every function above a limit you set. Working it out once by hand for a small function helps you understand what the number counts and why tools can disagree.
If I cover every path, is the function fully tested?
No. A path can pass with one set of data and fail with another, such as a division that fails only when a value is zero, and a requirement that was never implemented has no path at all. Path coverage measures routes through the code, not the correctness of results.
References
- 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.
- ISTQB Certified Tester Foundation Level (CTFL) v4.0The foundation syllabus of software testing: test levels and types, test design techniques, test management and tools.
Practise with Mutqan tools
Build decision tables from conditions and actions, generate all rule combinations and collapse impossible ones.
Generate boundary values (min-1, min, min+1, nominal, max-1, max, max+1) for numeric and length constraints.
Perform full boundary value analysis (2-value and 3-value BVA) with equivalence classes and expected results.
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.