03 Exercise: CFG Tracing
CFG Tracing Exercise: Visualizing Statement and Branch Coverage
Introduction
This exercise helps you understand code coverage by tracing test cases through Control Flow Graphs (CFGs). By visualizing which nodes (statements) and edges (branches) are covered, you’ll build intuition for how coverage works.
Preparation: Read First
Before attempting this exercise, study the following lecture sections:
From Chapter 03 (Testing Theory and Coverage): Testing Theory & Coverage:
- Section 4: Code Coverage (C0 and C1) - CFG examples, node/edge counting
- Section 5: Systematic Test Design for Branch Coverage - Deriving tests from CFGs
Learning Objectives
After completing these exercises, you should be able to:
- Read and interpret Control Flow Graphs
- Trace test case execution through code paths
- Calculate Statement Coverage (C0) from node coverage
- Calculate Branch Coverage (C1) from edge coverage
- Design tests to achieve specific coverage goals
- Understand why C1 subsumes C0
Instructions
- Study the code and CFG descriptions in each question
- Trace the given test case(s) through the graph
- Identify which nodes and edges are covered
- Calculate the coverage percentages
- Click to reveal explanations after answering
Time: 35-45 minutes for all exercises
Exercise 1: Basic CFG - classify_number()
Consider this code:
def classify_number(x):
if x > 0: # Node 1 (decision)
return "pos" # Node 2
elif x < 0: # Node 3 (decision)
return "neg" # Node 4
else:
return "zero" # Node 5
Test Case:
def test_positive():
assert classify_number(5) == "pos"
With input x = 5:
- Path: START → N1 (True) → N2 → END
- Nodes covered: N1, N2 (2 out of 5 nodes)
What is the Statement Coverage (C0)? Enter a number (no % sign).
Exercise 1: Basic CFG - classify_number() (continued)
Same code and test:
def test_positive():
assert classify_number(5) == "pos"
Branch edges (decision outcomes) in the CFG:
- N1 → N2 (True branch of
x > 0) - N1 → N3 (False branch of
x > 0) - N3 → N4 (True branch of
x < 0) - N3 → N5 (False branch of
x < 0)
Total branch edges: 4
The test covers only N1 → N2 (True branch).
What is the Branch Coverage (C1)? Enter a number (no % sign).
Exercise 2: Multiple Test Cases
Same classify_number() function, now with TWO tests:
def test_positive():
assert classify_number(5) == "pos"
def test_negative():
assert classify_number(-3) == "neg"
Traces:
test_positive: START → N1 (True) → N2 → END (covers N1, N2)test_negative: START → N1 (False) → N3 (True) → N4 → END (covers N1, N3, N4)
Combined nodes covered: N1, N2, N3, N4 (4 out of 5)
What is the combined Statement Coverage (C0)? Enter a number (no % sign).
Exercise 2: Multiple Test Cases (continued)
Branch edges covered by both tests:
- N1 → N2 (True): ✓ from test_positive
- N1 → N3 (False): ✓ from test_negative
- N3 → N4 (True): ✓ from test_negative
- N3 → N5 (False): ✗ not covered
What is the combined Branch Coverage (C1)? Enter a number (no % sign).
Exercise 2: What test is needed for 100% coverage?
Given the two tests (test_positive and test_negative), which additional test would achieve 100% branch coverage?
Exercise 3: calculate_grade() - Multiple Branches
def calculate_grade(score):
if score >= 90: # N1
return "A" # N2
elif score >= 80: # N3
return "B" # N4
elif score >= 70: # N5
return "C" # N6
elif score >= 60: # N7
return "D" # N8
else:
return "F" # N9
Two tests:
def test_grade_A():
assert calculate_grade(95) == "A" # Path: N1(T) → N2
def test_grade_B():
assert calculate_grade(85) == "B" # Path: N1(F) → N3(T) → N4
This function has 4 decision points (N1, N3, N5, N7), each with True/False outcomes. Total branches: 8
Branches covered:
- N1 True: ✓, N1 False: ✓, N3 True: ✓, N3 False: ✗
- N5 True: ✗, N5 False: ✗, N7 True: ✗, N7 False: ✗
What is the Branch Coverage (C1)? Enter the percentage as a decimal (e.g., 37.5).
Exercise 3: How many more tests needed?
To achieve 100% Branch Coverage (C1) for calculate_grade(), how many ADDITIONAL tests are needed beyond the two already provided (test_grade_A and test_grade_B)?
Enter a number.
Exercise 4: C0 vs C1 Comparison
def check_eligibility(age, has_license):
if age >= 18: # N1
if has_license: # N2
return "approved" # N3
return "need license" # N4
return "too young" # N5
Two tests:
def test_approved():
assert check_eligibility(25, True) == "approved"
# Path: N1(T) → N2(T) → N3
def test_need_license():
assert check_eligibility(20, False) == "need license"
# Path: N1(T) → N2(F) → N4
Coverage results:
- C0: 4/5 nodes = 80% (N1, N2, N3, N4 covered; N5 not covered)
- C1: 3/4 branches = 75% (N1 True, N2 True, N2 False covered; N1 False not covered)
Which statement is correct about C0 and C1?
Exercise 5: Nested Conditions - validate_password()
def validate_password(password):
if len(password) < 8: # N1
return "too short" # N2
has_upper = any(c.isupper() for c in password) # N3
has_digit = any(c.isdigit() for c in password) # N4
if not has_upper: # N5
return "need uppercase" # N6
if not has_digit: # N7
return "need digit" # N8
return "valid" # N9
Test:
def test_valid_password():
assert validate_password("SecurePass1") == "valid"
Trace: “SecurePass1” has length 11, uppercase letters, and a digit. Path: N1(F) → N3 → N4 → N5(F) → N7(F) → N9
Nodes covered: N1, N3, N4, N5, N7, N9 (6 out of 9)
What is C0? Enter a number (no % sign, round to nearest integer).
Exercise 5: Nested Conditions (continued)
Same test for validate_password():
Decision outcomes:
- N1 (len < 8): True = NOT covered, False = Covered ✓
- N5 (not has_upper): True = NOT covered, False = Covered ✓
- N7 (not has_digit): True = NOT covered, False = Covered ✓
What is C1? Enter a number (no % sign).
Exercise 5: Which tests are needed for 100% C1?
Select ALL tests that would help achieve 100% branch coverage for validate_password():
Exercise 6: Early Returns - calculate_discount()
def calculate_discount(amount, is_member, coupon_code):
if amount <= 0: # N1
return 0 # N2
discount = 0 # N3
if is_member: # N4
discount += 10 # N5
if coupon_code == "SAVE20": # N6
discount += 20 # N7
elif coupon_code == "SAVE10": # N8
discount += 10 # N9
return amount * (discount / 100) # N10
Two tests:
def test_member_with_save20():
result = calculate_discount(100, True, "SAVE20")
# Path: N1(F) → N3 → N4(T) → N5 → N6(T) → N7 → N10
def test_non_member_no_coupon():
result = calculate_discount(100, False, None)
# Path: N1(F) → N3 → N4(F) → N6(F) → N8(F) → N10
Nodes covered: N1, N3, N4, N5, N6, N7, N8, N10 (8 out of 10)
What is C0? Enter a number (no % sign).
Exercise 6: Which branches are NOT covered?
Same tests as above. Select ALL uncovered branches:
Exercise 7: Complex Conditions - approve_loan()
def approve_loan(income, credit_score, has_collateral):
if income < 30000: # N1
return "rejected: low income" # N2
if credit_score < 600: # N3
return "rejected: poor credit" # N4
if income >= 100000 and credit_score >= 750: # N5
return "approved: premium" # N6
if credit_score >= 700 or has_collateral: # N7
return "approved: standard" # N8
return "manual review required" # N9
Three tests:
def test_premium_approval():
assert approve_loan(150000, 800, False) == "approved: premium"
# Path: N1(F) → N3(F) → N5(T) → N6
def test_standard_with_good_credit():
assert approve_loan(50000, 720, False) == "approved: standard"
# Path: N1(F) → N3(F) → N5(F) → N7(T) → N8
def test_standard_with_collateral():
assert approve_loan(50000, 650, True) == "approved: standard"
# Path: N1(F) → N3(F) → N5(F) → N7(T) → N8
Nodes covered: N1, N3, N5, N6, N7, N8 (6 out of 9)
What is C0? Enter a number (no % sign, round to nearest integer).
Exercise 7: Complex Conditions (continued)
Branch outcomes:
- N1 (income < 30k): True = ✗, False = ✓
- N3 (credit < 600): True = ✗, False = ✓
- N5 (income>=100k AND credit>=750): True = ✓, False = ✓
- N7 (credit>=700 OR collateral): True = ✓, False = ✗
What is C1? Enter the percentage as a decimal (e.g., 62.5).
Exercise 8: Design Your Own Tests
def process_order(total, user_type, promo_code):
if total <= 0: # N1
return "invalid order" # N2
base_discount = 0 # N3
if user_type == "premium": # N4
base_discount = 15 # N5
elif user_type == "member": # N6
base_discount = 5 # N7
if promo_code == "FLASH50": # N8
if total >= 100: # N9
return f"discount: 50%" # N10
return "promo requires $100+" # N11
if base_discount > 0: # N12
return f"discount: {base_discount}%" # N13
return "no discount" # N14
How many decision points (if/elif statements) does this code have?
Enter a number.
Exercise 8: Minimum Tests for 100% C1
The process_order() function has 6 decision points with 12 total branches.
What is the minimum number of tests needed for 100% Branch Coverage (C1)?
Enter a number.
Summary: C1 Subsumes C0
Based on the exercises, which statement about coverage relationships is TRUE?
Summary: Key Insight from CFG Tracing
What is the most important insight from tracing tests through Control Flow Graphs?