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%

flowchart TD A[Start] --> B[discount = 0] B --> C{is_member?} C -->|True ✅ TC1| D[discount = price * 0.10] C -->|False ✅ TC2| E[final_price = price - discount] D --> E E --> F[return final_price]

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:

TCInputD1D2D3StatementsReturn
TC1age = -5T--S1, S2“Invalid”
TC2age = 10FT-S1, S3“Minor”
TC3age = 30FFTS1, S4“Adult”
TC4age = 70FFFS1, 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 > 0 True and False

Test cases for 100% decision coverage:

TCInputD1D2Statements
TC1[]F (skip)-S1, S2, S4
TC2[5, -3]T (enter)T, FS1, 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 TypeWhat It MeansRisk
Uncovered statementCode exists that no test executesDead code or untested logic
Uncovered True branchThe condition was never true in testsPositive path not tested
Uncovered False branchThe condition was never false in testsError/default path not tested

Limitations of Statement and Decision Coverage

What 100% coverage does NOT guarantee:

  1. Missing code. Coverage measures what’s there, not what’s missing. A missing null check won’t show as uncovered.

  2. 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 >=.

  3. Combination defects. Two independent decisions might interact in unexpected ways that neither statement nor decision coverage catches.

  4. 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:

TCamountmethodcurrencyCovers
TC1-10anyanyD1-T
TC2100credit_cardUSDD1-F, D2-T, D4-F
TC3100bank_transferEURD1-F, D2-F, D3-T, D4-T
TC4100paypalUSDD1-F, D2-F, D3-F

4 test cases for 100% decision coverage.

Tools for Measuring Coverage

LanguageToolCommand
Pythoncoverage.pycoverage run -m pytest && coverage report
JavaScriptIstanbul/nycnyc mocha
JavaJaCoCoIntegrated with Maven/Gradle
GoBuilt-ingo test -cover
C#dotCoverVisual 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:

  1. Identify all decisions and their outcomes
  2. Design the minimum test set for 100% decision coverage
  3. 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):

TCInputD1D2D3D4Return
TC1"short"T---Too short
TC2"alllowercase1"FT--Needs uppercase
TC3"Alluppernone"FFT-Needs digit
TC4"A" + "a"*64 + "1" (66 chars)FFFTToo long
TC5"ValidPass1"FFFFValid

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.