What Are Statement and Decision Coverage?
Statement and decision coverage are white-box (structure-based) test design techniques that measure how thoroughly test cases exercise the source code. Unlike black-box techniques that focus on requirements, these techniques focus on code structure.
Why Code Coverage Matters
Code that’s never executed during testing is code that’s never verified. Coverage metrics tell you:
- Which lines of code your tests actually run
- Which branches of decision points remain untested
- Where to add tests for better structural coverage
Statement Coverage
Definition: The percentage of executable statements executed by the test suite.
Statement Coverage = (Statements Executed / Total Executable Statements) x 100%
Example:
def calculate_discount(price, is_member): # Line 1
discount = 0 # Line 2
if is_member: # Line 3
discount = price * 0.10 # Line 4
final_price = price - discount # Line 5
return final_price # Line 6
Test case 1: calculate_discount(100, True) → Executes lines 1, 2, 3, 4, 5, 6
Statement coverage = 6/6 = 100%
But wait — we only tested with is_member=True. We never tested the case where the member check is false. This is the weakness of statement coverage.
Decision Coverage (Branch Coverage)
Definition: The percentage of decision outcomes (true/false branches) executed by the test suite.
Decision Coverage = (Decision Outcomes Executed / Total Decision Outcomes) x 100%
For the same code:
- Decision at line 3:
is_member→ has 2 outcomes: True and False
Test case 1: calculate_discount(100, True) → Decision is True
Test case 2: calculate_discount(100, False) → Decision is False
Decision coverage = 2/2 = 100%
Relationship: Decision Coverage Subsumes Statement Coverage
100% decision coverage → 100% statement coverage (always true)
100% statement coverage → 100% decision coverage (NOT always true)
This is because achieving both True and False for every decision ensures that all code blocks (including else branches) are executed.
Calculating Coverage: Step by Step
def categorize_age(age): # S1
if age < 0: # D1
return "Invalid" # S2
elif age < 18: # D2
return "Minor" # S3
elif age < 65: # D3
return "Adult" # S4
else: # D3-false
return "Senior" # S5
Statements: S1, S2, S3, S4, S5 (5 total) Decisions: D1 (T/F), D2 (T/F), D3 (T/F) (6 outcomes total)
Minimum test cases for 100% decision coverage:
| TC | Input | D1 | D2 | D3 | Statements | Return |
|---|---|---|---|---|---|---|
| TC1 | age = -5 | T | - | - | S1, S2 | “Invalid” |
| TC2 | age = 10 | F | T | - | S1, S3 | “Minor” |
| TC3 | age = 30 | F | F | T | S1, S4 | “Adult” |
| TC4 | age = 70 | F | F | F | S1, S5 | “Senior” |
4 test cases achieve 100% statement and 100% decision coverage.
Coverage in Loops
def sum_positives(numbers): # S1
total = 0 # S2
for n in numbers: # D1 (loop: enter/skip)
if n > 0: # D2
total += n # S3
return total # S4
Decisions:
- D1: Loop entered (True) and loop skipped/exited (False)
- D2:
n > 0True and False
Test cases for 100% decision coverage:
| TC | Input | D1 | D2 | Statements |
|---|---|---|---|---|
| TC1 | [] | F (skip) | - | S1, S2, S4 |
| TC2 | [5, -3] | T (enter) | T, F | S1, S2, S3, S4 |
2 test cases for 100% decision coverage.
Advanced Coverage Analysis
Coverage Gaps and What They Mean
When coverage is below 100%, the uncovered code reveals:
| Gap Type | What It Means | Risk |
|---|---|---|
| Uncovered statement | Code exists that no test executes | Dead code or untested logic |
| Uncovered True branch | The condition was never true in tests | Positive path not tested |
| Uncovered False branch | The condition was never false in tests | Error/default path not tested |
Limitations of Statement and Decision Coverage
What 100% coverage does NOT guarantee:
Missing code. Coverage measures what’s there, not what’s missing. A missing
nullcheck won’t show as uncovered.Data-dependent defects.
if (x > 0)with x=1 and x=-1 gives 100% decision coverage, but misses the boundary defect if the condition should be>=.Combination defects. Two independent decisions might interact in unexpected ways that neither statement nor decision coverage catches.
Non-functional issues. Performance, security, and usability defects are invisible to code coverage.
Real-World Example: Payment Processing
def process_payment(amount, method, currency):
if amount <= 0: # D1
raise ValueError("Invalid amount")
if method == "credit_card": # D2
fee = amount * 0.029 # 2.9% fee
elif method == "bank_transfer": # D3
fee = 1.50 # flat fee
else:
raise ValueError("Unsupported method")
if currency != "USD": # D4
fee += 0.50 # currency conversion fee
return amount + fee
Decisions: D1 (T/F), D2 (T/F), D3 (T/F), D4 (T/F) = 8 outcomes
Minimum test set for 100% decision coverage:
| TC | amount | method | currency | Covers |
|---|---|---|---|---|
| TC1 | -10 | any | any | D1-T |
| TC2 | 100 | credit_card | USD | D1-F, D2-T, D4-F |
| TC3 | 100 | bank_transfer | EUR | D1-F, D2-F, D3-T, D4-T |
| TC4 | 100 | paypal | USD | D1-F, D2-F, D3-F |
4 test cases for 100% decision coverage.
Tools for Measuring Coverage
| Language | Tool | Command |
|---|---|---|
| Python | coverage.py | coverage run -m pytest && coverage report |
| JavaScript | Istanbul/nyc | nyc mocha |
| Java | JaCoCo | Integrated with Maven/Gradle |
| Go | Built-in | go test -cover |
| C# | dotCover | Visual Studio integration |
Exercise: Design Tests for Coverage
Scenario: Analyze this function and design the minimum test set for 100% decision coverage:
def validate_password(password):
if len(password) < 8:
return {"valid": False, "error": "Too short"}
has_upper = any(c.isupper() for c in password)
has_digit = any(c.isdigit() for c in password)
if not has_upper:
return {"valid": False, "error": "Needs uppercase"}
if not has_digit:
return {"valid": False, "error": "Needs digit"}
if len(password) > 64:
return {"valid": False, "error": "Too long"}
return {"valid": True, "error": None}
Tasks:
- Identify all decisions and their outcomes
- Design the minimum test set for 100% decision coverage
- Calculate statement coverage for your test set
Hint
There are 4 decision points (4 if statements), each with True and False outcomes = 8 total outcomes. Think about what input triggers each True and each False. Note: the function returns early on some True branches, so you need to think about which decisions are reachable.
Solution
Decisions:
- D1:
len(password) < 8(T/F) - D2:
not has_upper(T/F) - D3:
not has_digit(T/F) - D4:
len(password) > 64(T/F)
Minimum test set (5 test cases):
| TC | Input | D1 | D2 | D3 | D4 | Return |
|---|---|---|---|---|---|---|
| TC1 | "short" | T | - | - | - | Too short |
| TC2 | "alllowercase1" | F | T | - | - | Needs uppercase |
| TC3 | "Alluppernone" | F | F | T | - | Needs digit |
| TC4 | "A" + "a"*64 + "1" (66 chars) | F | F | F | T | Too long |
| TC5 | "ValidPass1" | F | F | F | F | Valid |
Statement coverage: 100% (all return statements and assignments executed) Decision coverage: 100% (all 8 outcomes covered: D1-T, D1-F, D2-T, D2-F, D3-T, D3-F, D4-T, D4-F)
Note: 5 test cases are needed (not 4) because the early returns mean some decisions are only reachable when previous decisions are False.
Pro Tips
- Use coverage as a guide, not a goal. 100% coverage doesn’t mean 100% quality. But low coverage definitely means low confidence.
- Focus on decision coverage over statement. It’s a stronger criterion that costs little extra effort.
- Investigate uncovered branches. Sometimes uncovered code is dead code that should be removed. Other times it’s critical error handling that needs tests.
- Combine with black-box techniques. Coverage tells you what code ran; EP/BVA tells you what values to test. Use both for maximum effectiveness.
- Set realistic targets. 80% coverage is practical for most projects. The last 20% often includes error handlers, platform-specific code, and defensive checks that are hard to trigger in unit tests.