03 Übung: Coverage Detektiv
Anleitung
In dieser Übung wirst du zum Coverage-Detektiv. Du analysierst echte pytest-cov-Ausgaben, identifizierst welche Code-Pfade ungetestet sind und entwirfst Tests, um die Lücken zu schließen.
Vorbereitung: Zuerst lesen
Bevor du diese Übung versuchst, lies die folgenden Vorlesungsabschnitte:
Aus Kapitel 03 (TDD und CI): TDD und CI:
- Teil 3: Coverage Reporting einrichten -
--cov-report=term-missingverstehen
Aus Kapitel 03 (Testtheorie und Coverage): Testtheorie & Coverage:
- Abschnitt 5: Systematischer Testentwurf - Tests für spezifische Branches entwerfen
Lernziele
- pytest-cov-Ausgabe interpretieren und fehlende Zeilen identifizieren
- Verstehen, warum bestimmte Zeilen nicht ausgeführt wurden
- Testfälle entwerfen, die spezifische Branches abdecken
- Testfunktionen schreiben, die Ziel-Coverage erreichen
Struktur
- Teil 1 (Fragen 1-5): Analyse - Identifizieren was fehlt und warum
- Teil 2 (Fragen 6-10): Entwurf - Tests erstellen, um die Lücken zu schließen
Bewertungsleitfaden
- 9-10 richtig: Ausgezeichnet! Bereit für die GitHub Classroom-Aufgabe
- 7-8 richtig: Gutes Verständnis! Lies die Erklärungen
- 5-6 richtig: Ausreichendes Verständnis. Schau dir die Vorlesungsmaterialien an
- Unter 5: Bitte wiederhole die Coverage-Konzepte bevor du fortfährst
GitHub Classroom-Aufgabe
Nach Abschluss dieses Quiz wende deine Fähigkeiten in einer echten praktischen Aufgabe an:
Coverage-Verbesserungsübung akzeptieren
Du wirst Coverage von ~54% auf 85%+ verbessern mit sofortigem CI-Feedback!
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
Welche Bedingungen sind NICHT abgedeckt, und warum?
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
Zeile 3 ist return "Invalid name". Warum ist sie NICHT abgedeckt?
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 (teilweise)
Zeile 11-12 ist als teilweise abgedeckt markiert. Warum?
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
Zeilen 6-7 sind der except TypeError-Block. Welche Eingabe würde diese Exception auslösen?
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
Zeile 3 ist return None. Warum ist die Schleife (Zeilen 6-8) als abgedeckt angezeigt, aber Zeile 3 nicht?
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
Welcher Test würde Zeile 15 (return "needs lowercase") abdecken?
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
Um Zeilen 17-18 (Rabatt für subtotal > 50) abzudecken, was ist das MINIMALE Gewicht für domestic non-express?
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
Welche Eingabe würde Zeile 22 (raise ValueError("Empty key not allowed")) abdecken?
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% Mitgliederrabatt
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: # Rabatt auf 50% begrenzen
22: if discount > 0.50:
23: discount = 0.50
24:
25: return amount * discount
Coverage-Report: Missing: 3, 14, 17, 19, 23
Kann Zeile 23 (Rabatt-Cap = 0.50) mit diesem Code jemals erreicht werden?
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", ...}
Um Zeile 23 (Rabatt-Cap bei 60%) auszulösen, welche Kombination funktioniert?