Privacy Notice: When you sign in with GitHub, your quiz attempts are stored in Google Firebase so you can track your progress. This data is only accessible to the course instructor, is not otherwise shared, and will be deleted at the end of the semester. For earlier deletion, contact the instructor. Prefer to stay anonymous? Use guest mode or a GitHub account without your real name.
Loading...

03 Exercise: Coverage Detective

Score: 0 / 10

Instructions

In this exercise, you’ll become a Coverage Detective. You’ll analyze real pytest-cov output, identify which code paths are untested, and design tests to cover the gaps.


Preparation: Read First

Before attempting this exercise, study the following lecture sections:

From Chapter 03 (TDD and CI): TDD and CI:

From Chapter 03 (Testing Theory and Coverage): Testing Theory & Coverage:


Learning Objectives

  • Interpret pytest-cov output and identify missing lines
  • Understand why specific lines weren’t executed
  • Design test cases that cover specific branches
  • Write test functions that achieve target coverage

Structure

  • Part 1 (Questions 1-5): Analysis - Identify what’s missing and why
  • Part 2 (Questions 6-10): Design - Create tests to cover the gaps

Scoring Guide

  • 9-10 correct: Excellent! Ready for the GitHub Classroom assignment
  • 7-8 correct: Good understanding! Review the explanations
  • 5-6 correct: Fair understanding. Revisit the lecture materials
  • Below 5: Please review the coverage concepts before continuing

GitHub Classroom Assignment

After completing this quiz, apply your skills in a real hands-on assignment:

Accept the Coverage Improvement Exercise

You’ll improve coverage from ~54% to 85%+ with immediate CI feedback!

Question1 Part 1: Analysis

Code: validate_age.py

1: def validate_age(age):
2:     if age < 0:
3:         return "invalid: negative"
4:     if age < 18:
5:         return "minor"
6:     if age < 65:
7:         return "adult"
8:     return "senior"

Tests:

def test_minor():
    assert validate_age(15) == "minor"

def test_adult():
    assert validate_age(30) == "adult"

Coverage Report: Missing: 3, 8

What conditions are NOT covered, and why?

Question2 Part 1: Analysis

Code: format_name.py

1: def format_name(first, last, formal=False):
2:     if not first or not last:
3:         return "Invalid name"
4:
5:     if formal:
6:         return f"Mr./Ms. {last}"
7:     return f"{first} {last}"

Tests:

def test_informal_name():
    assert format_name("John", "Doe") == "John Doe"

def test_formal_name():
    assert format_name("Jane", "Smith", formal=True) == "Mr./Ms. Smith"

Coverage Report: Missing: 3

Line 3 is return "Invalid name". Why is it NOT covered?

Question3 Part 1: Analysis

Code: check_access.py

1:  def check_access(user_role, resource_type, is_owner):
2:      # Admin can access anything
3:      if user_role == "admin":
4:          return True
5:
6:      # Owner can access their own resources
7:      if is_owner:
8:          return True
9:
10:     # Editors can access documents and images
11:     if user_role == "editor" and resource_type in ["document", "image"]:
12:         return True
13:
14:     return False

Tests:

def test_admin_access():
    assert check_access("admin", "secret", False) == True

def test_owner_access():
    assert check_access("viewer", "document", True) == True

def test_editor_document():
    assert check_access("editor", "document", False) == True

def test_denied():
    assert check_access("viewer", "document", False) == False

Coverage Report: Missing: 12 (partial)

Line 11-12 is marked as partially covered. Why?

Question4 Part 1: Analysis

Code: divide.py

1: def safe_divide(a, b):
2:     try:
3:         result = a / b
4:     except ZeroDivisionError:
5:         return None
6:     except TypeError:
7:         return "invalid input"
8:     return result

Tests:

def test_normal_division():
    assert safe_divide(10, 2) == 5.0

def test_divide_by_zero():
    assert safe_divide(10, 0) is None

Coverage Report: Missing: 7, 8

Lines 6-7 are the except TypeError block. What input would trigger this exception?

Question5 Part 1: Analysis

Code: find_max.py

1: def find_max(numbers):
2:     if not numbers:
3:         return None
4:
5:     max_val = numbers[0]
6:     for num in numbers[1:]:
7:         if num > max_val:
8:             max_val = num
9:     return max_val

Tests:

def test_single_element():
    assert find_max([42]) == 42

def test_sorted_ascending():
    assert find_max([1, 2, 3, 4, 5]) == 5

Coverage Report: Missing: 3

Line 3 is return None. Why is the loop (lines 6-8) shown as covered but line 3 is not?

Question6 Part 2: Design Tests

Code: validate_password.py

1:  def validate_password(password):
2:      if len(password) < 8:
3:          return "too short"
4:
5:      if len(password) > 128:
6:          return "too long"
7:
8:      has_upper = any(c.isupper() for c in password)
9:      has_lower = any(c.islower() for c in password)
10:     has_digit = any(c.isdigit() for c in password)
11:
12:     if not has_upper:
13:         return "needs uppercase"
14:     if not has_lower:
15:         return "needs lowercase"
16:     if not has_digit:
17:         return "needs digit"
18:
19:     return "valid"

Coverage Report: Missing: 6, 15, 17

Which test would cover line 15 (return "needs lowercase")?

Question7 Part 2: Design Tests

Code: shipping.py

1:  def calculate_shipping(weight, destination, express=False):
2:      if weight <= 0:
3:          raise ValueError("Weight must be positive")
4:
5:      if destination not in ["domestic", "international"]:
6:          raise ValueError("Invalid destination")
7:
8:      base_rate = 5.0 if destination == "domestic" else 15.0
9:      weight_charge = weight * 0.5
10:
11:     subtotal = base_rate + weight_charge
12:
13:     if express:
14:         subtotal *= 1.5
15:
16:     if subtotal > 50:
17:         discount = subtotal * 0.1
18:         return subtotal - discount
19:
20:     return subtotal

Coverage Report: Missing: 3, 6, 17, 18

To cover lines 17-18 (discount for subtotal > 50), what’s the MINIMUM weight for domestic non-express?

Question8 Part 2: Design Tests

Code: config.py

1:  def parse_config(config_string):
2:      if not config_string:
3:          return {}
4:
5:      result = {}
6:      lines = config_string.strip().split('\n')
7:
8:      for line in lines:
9:          line = line.strip()
10:
11:         if not line or line.startswith('#'):
12:             continue
13:
14:         if '=' not in line:
15:             raise ValueError(f"Invalid config line: {line}")
16:
17:         key, value = line.split('=', 1)
18:         key = key.strip()
19:         value = value.strip()
20:
21:         if not key:
22:             raise ValueError("Empty key not allowed")
23:
24:         result[key] = value
25:
26:     return result

Coverage Report: Missing: 3, 12, 15, 22

Which input would cover line 22 (raise ValueError("Empty key not allowed"))?

Question9 Part 2: Design Tests

Code: discount.py

1:  def calculate_discount(amount, is_member, coupon_code):
2:      if amount <= 0:
3:          return 0
4:
5:      discount = 0.0
6:
7:      if is_member:
8:          discount += 0.10  # 10% member discount
9:
10:     if coupon_code is not None:
11:         if coupon_code == "SAVE20":
12:             discount += 0.20
13:         elif coupon_code == "SAVE10":
14:             discount += 0.10
15:         elif coupon_code.startswith("VIP"):
16:             if is_member:
17:                 discount += 0.30
18:             else:
19:                 discount += 0.15
20:
21:     # Cap discount at 50%
22:     if discount > 0.50:
23:         discount = 0.50
24:
25:     return amount * discount

Coverage Report: Missing: 3, 14, 17, 19, 23

Can line 23 (discount cap = 0.50) ever be reached with this code?

Question10 Part 2: Design Tests

Code: process_order.py

1:  def process_order(items, customer_type, promo_code=None):
2:      if not items:
3:          return {"status": "error", "message": "No items"}
4:
5:      subtotal = sum(item["price"] * item["quantity"] for item in items)
6:
7:      if subtotal <= 0:
8:          return {"status": "error", "message": "Invalid total"}
9:
10:     discount_rate = 0
11:
12:     if customer_type == "premium":
13:         discount_rate = 0.15
14:     elif customer_type == "member":
15:         discount_rate = 0.05
16:
17:     if promo_code == "FLASH50" and subtotal >= 100:
18:         discount_rate += 0.50
19:     elif promo_code == "FLASH25" and subtotal >= 50:
20:         discount_rate += 0.25
21:
22:     if discount_rate > 0.60:
23:         discount_rate = 0.60
24:
25:     discount = subtotal * discount_rate
26:     total = subtotal - discount
27:
28:     return {"status": "success", ...}

To trigger line 23 (discount cap at 60%), which combination works?

© 2026 Dominik Mueller   •  Powered by Soopr   •  Theme  Moonwalk