03 Grundlagen des Testens: Automatisiertes Testen in CI & Test-Coverage
December 2025 (9793 Words, 55 Minutes)
1. Einführung: Ihre CI prüft den Stil, aber prüft sie auch die Korrektheit?
In Kapitel 02 (Feature-Entwicklung) haben Sie gelernt, wie man GitHub Actions CI/CD einrichtet, um Code-Qualität automatisch zu überprüfen:
- ✅ Ruff Linter: Fängt Stilprobleme ab
- ✅ Ruff Format: Erzwingt konsistente Formatierung
- ✅ Pyright: Fängt Typ-Fehler ab
In Kapitel 03 (Grundlagen des Testens) haben Sie gelernt, wie man Tests mit pytest schreibt:
- ✅ Unit-Tests: Testen einzelne Funktionen isoliert
- ✅ Äquivalenzklassen: Gruppieren ähnlicher Eingaben
- ✅ Grenzwert-Analyse: Testen von Edge Cases
- ✅ LLM-Unterstützung: Beschleunigen der Testerstellung
Aber hier ist das Problem:
# Sie schreiben großartige Tests
$ pytest tests/
================================ test session starts =================================
collected 47 items
tests/test_geometry.py ...... [ 12%]
tests/test_road.py ......... [ 31%]
tests/test_visualization.py .... [ 40%]
tests/test_integration.py .......... [ 61%]
tests/test_edge_cases.py .................. [100%]
================================ 47 passed in 2.34s ==================================
# Sie pushen zur CI...
$ git push origin feature/add-input-validation
# GitHub Actions läuft...
✅ Ruff linter - PASSED
✅ Ruff format - PASSED
✅ Pyright - PASSED
# Aber Ihre Tests? Wurden nie ausgeführt! 😱
Die Lücke:
- Ihre CI stellt sicher, dass der Code gut aussieht (Formatierung, Stil, Typen)
- Aber sie stellt nicht sicher, dass der Code funktioniert (Korrektheit, Logik, Verhalten)
Was Sie in dieser Vorlesung lernen werden:
- CI + Tests: Wie man pytest zu GitHub Actions hinzufügt, um Tests obligatorisch zu machen
- Test-Coverage: Wie man messbar macht, welcher Code getestet ist (und welcher nicht)
- Test-Driven Development (TDD): Eine optionale Disziplin, die beim Schreiben von besserem Code hilft
Pädagogischer Hinweis: Erst Praxis, dann Theorie
In diesem Kurs folgen wir einem “Praxis zuerst”-Ansatz für das Testen:
- Kapitel 03 (Grundlagen des Testens) hat Ihnen beigebracht, wie man Tests schreibt (pytest-Grundlagen, Äquivalenzklassen, Grenzwerte)
- Kapitel 03 (TDD und CI) (diese Vorlesung) lehrt Sie, wie man Tests automatisiert (CI-Integration, Coverage-Messung)
- Kapitel 03 (Testtheorie und Coverage) lehrt Sie, warum Coverage funktioniert (formale Theorie, C0 vs C1, systematisches Testdesign)
Dies spiegelt wider, wie professionelle Entwickler lernen: erst die Dinge zum Laufen bringen, dann die Theorie dahinter verstehen. Wenn Sie Kapitel 03 (Testtheorie und Coverage) erreichen, haben Sie bereits praktische Erfahrung mit Coverage-Metriken, was die Theorie bedeutungsvoller macht.
Was Sie in der NÄCHSTEN Vorlesung (Kapitel 03 (Testtheorie und Coverage)) lernen werden:
- Das formale theoretische Framework für Tests (Program, Model, Coverage Criterion)
- Der Unterschied zwischen Statement (C0) und Branch (C1) Coverage
- Wie man systematisch Tests entwirft, um spezifische Coverage-Ziele zu erreichen
- Warum Äquivalenzklassen und Grenzwerte auch “Coverage-Kriterien” sind
2. Teil 1: Hinzufügen von pytest zu GitHub Actions CI
2.1. Warum Tests in CI laufen müssen
Szenario: Sie sind Teil eines Teams mit 5 Entwicklern. Jeder arbeitet an Feature-Branches:
# Entwickler A
$ git checkout -b feature/add-angle-validation
# ... ändert geometry.py ...
$ pytest tests/ # ✅ Alle Tests bestehen lokal
$ git push origin feature/add-angle-validation
# Entwickler B (gleichzeitig)
$ git checkout -b feature/optimize-ray-calculation
# ... ändert geometry.py (dieselbe Datei!) ...
$ pytest tests/ # ✅ Alle Tests bestehen lokal
$ git push origin feature/optimize-ray-calculation
Was kann schiefgehen?
- Entwickler A’s PR wird gemerged →
mainBranch wird aktualisiert - Entwickler B’s Branch ist jetzt veraltet (basiert auf alter
main) - Entwickler B’s PR wird gemerged → Keine Konflikte (verschiedene Zeilen)
- Aber kombiniert brechen die Änderungen die Tests! 💥
Ohne CI-Tests:
- Niemand bemerkt das Problem, bis jemand
mainpullt und Tests lokal lauft - Könnte Stunden oder Tage später sein
mainBranch ist jetzt kaputt- Jeder der pullt hat kaputten Code
Mit CI-Tests:
- Entwickler B’s PR schlägt in CI fehl (Tests schlagen fehl)
- PR kann nicht gemerged werden, bis sie gefixed ist
mainbleibt immer funktionsfähig- Problem wird innerhalb von Minuten gefangen, nicht Tagen
Ziel: Machen Sie Tests obligatorisch indem Sie sie in CI laufen lassen.
2.2. Ihre aktuelle CI-Pipeline (aus Kapitel 02)
Hier ist die .github/workflows/quality.yml die Sie in Kapitel 02 (Feature-Entwicklung) erstellt haben:
name: Code Quality Checks
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
quality:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
version: "latest"
- name: Install dependencies
run: uv sync
- name: Run Ruff linter
run: uv run ruff check code_examples/
- name: Run Ruff formatter
run: uv run ruff format --check code_examples/
- name: Run Pyright
run: uv run pyright code_examples/
Was diese Pipeline macht:
- ✅ Prüft Code-Stil (Ruff linter)
- ✅ Prüft Formatierung (Ruff formatter)
- ✅ Prüft Typen (Pyright)
Was sie NICHT macht:
- ❌ Führt Tests aus (pytest)
- ❌ Misst Test-Coverage
- ❌ Verifiziert, dass der Code funktioniert
2.3. Hinzufügen von pytest zu Ihrer CI-Pipeline
Schritt 1: Fügen Sie einen pytest-Schritt zu Ihrem Workflow hinzu
name: Code Quality Checks
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
quality:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
version: "latest"
- name: Install dependencies
run: uv sync
- name: Run Ruff linter
run: uv run ruff check code_examples/
- name: Run Ruff formatter
run: uv run ruff format --check code_examples/
- name: Run Pyright
run: uv run pyright code_examples/
# 🆕 NEU: Führe Tests aus
- name: Run tests
run: uv run pytest tests/ -v
Was ändert sich:
- Neuer Schritt:
Run tests - Befehl:
uv run pytest tests/ -v-v: Verbose-Ausgabe (zeigt jeden Test-Namen)tests/: Führe alle Tests imtests/Verzeichnis aus
Was passiert jetzt:
- Jeder PR muss alle Tests bestehen, bevor er gemerged werden kann
- Wenn ein Test fehlschlägt → CI schlägt fehl → PR kann nicht gemerged werden
mainBranch bleibt immer funktionsfähig (alle Tests bestehen)
2.4. Testen der neuen Pipeline
Schritt 1: Erstelle einen Feature-Branch
$ git checkout -b feature/add-pytest-to-ci
Schritt 2: Aktualisiere .github/workflows/quality.yml
Füge den pytest-Schritt wie oben gezeigt hinzu.
Schritt 3: Commit und Push
$ git add .github/workflows/quality.yml
$ git commit -m "Add pytest to CI pipeline"
$ git push origin feature/add-pytest-to-ci
Schritt 4: Erstelle einen Pull Request
Auf GitHub:
- Gehen Sie zu Ihrem Repository
- Klicke “Pull requests” → “New pull request”
- Wähle
feature/add-pytest-to-ci→main - Klicke “Create pull request”
Schritt 5: Beobachte GitHub Actions
Die CI wird automatisch laufen:
Code Quality Checks
├─ Checkout code ✅ PASSED
├─ Set up Python ✅ PASSED
├─ Install uv ✅ PASSED
├─ Install dependencies ✅ PASSED
├─ Run Ruff linter ✅ PASSED
├─ Run Ruff formatter ✅ PASSED
├─ Run Pyright ✅ PASSED
└─ Run tests ✅ PASSED (47 tests in 2.34s)
Wenn alle Checks grün sind:
- ✅ PR kann gemerged werden
- ✅
mainBranch bleibt funktionsfähig
Wenn ein Test fehlschlägt:
- ❌ CI schlägt fehl
- ❌ PR kann nicht gemerged werden (Branch Protection Rules)
- 🔧 Entwickler muss den Test fixen, bevor merge erlaubt ist
2.5. Branch Protection Rules: Tests obligatorisch machen
Problem: Auch mit Tests in CI könnte jemand die Checks umgehen und direkt zu main pushen.
Lösung: Branch Protection Rules erzwingen, dass alle CI-Checks bestehen müssen.
Schritt 1: Aktiviere Branch Protection auf GitHub
- Gehen Sie zu Ihrem Repository auf GitHub
- Klicken Sie Settings → Branches
- Klicken Sie Add rule unter “Branch protection rules”
- Branch name pattern:
main - Aktivieren Sie diese Einstellungen:
- ✅ Require a pull request before merging
- ✅ Require status checks to pass before merging
- Suchen und fügen Sie hinzu:
quality(der Job-Name aus Ihrem Workflow)
- Suchen und fügen Sie hinzu:
- ✅ Require branches to be up to date before merging
- ✅ Include administrators (gilt für jeden, auch Sie!)
- Klicken Sie Create
Was das macht:
- 🚫 Niemand kann direkt zu
mainpushen - 🚫 PRs können nicht gemerged werden, wenn Tests fehlschlagen
- ✅
mainbleibt immer in einem funktionsfähigen Zustand
2.6. Beispiel: Was passiert, wenn ein Test fehlschlägt
Szenario: Sie änderst geometry.py, aber brichst versehentlich einen Test.
# code_examples/geometry.py
def find_intersection(x_road, y_road, angle_degrees, camera_x=0, camera_y=1.5):
"""Find intersection of camera ray with road profile."""
# 🐛 BUG: Vergessen zu validieren, dass Arrays nicht leer sind
# if len(x_road) == 0 or len(y_road) == 0:
# return None
angle_rad = -np.deg2rad(angle_degrees)
slope = np.tan(angle_rad)
intercept = camera_y - slope * camera_x
# ... Rest der Funktion ...
Sie pushen die Änderung:
$ git add code_examples/geometry.py
$ git commit -m "Optimize intersection calculation"
$ git push origin feature/optimize-intersection
GitHub Actions führt Tests aus:
Run tests
============================= test session starts ==============================
collected 47 items
tests/test_geometry.py .....F [ 12%]
tests/test_road.py ......... [ 31%]
tests/test_visualization.py .... [ 40%]
tests/test_integration.py .......... [ 61%]
tests/test_edge_cases.py .................. [100%]
=================================== FAILURES ===================================
___________ test_find_intersection_empty_arrays ___________
def test_find_intersection_empty_arrays():
"""Test that empty arrays are handled gracefully."""
x_road = np.array([])
y_road = np.array([])
result = find_intersection(x_road, y_road, angle_degrees=-5.0)
> assert result is None, "Should return None for empty arrays"
E IndexError: index 0 is out of bounds for axis 0 with size 0
tests/test_geometry.py:45: IndexError
=========================== 1 failed, 46 passed in 2.41s ===========================
Error: Process completed with exit code 1.
Resultat:
- ❌ CI schlägt fehl (1 Test schlägt fehl)
- ❌ PR kann nicht gemerged werden
- 🔧 Sie müssen den Bug fixen, bevor merge erlaubt ist
Der Fix:
# code_examples/geometry.py
def find_intersection(x_road, y_road, angle_degrees, camera_x=0, camera_y=1.5):
"""Find intersection of camera ray with road profile."""
# ✅ FIX: Validiere leere Arrays
if len(x_road) == 0 or len(y_road) == 0:
return None
angle_rad = -np.deg2rad(angle_degrees)
slope = np.tan(angle_rad)
intercept = camera_y - slope * camera_x
# ... Rest der Funktion ...
Sie pushen den Fix:
$ git add code_examples/geometry.py
$ git commit -m "Fix: Handle empty arrays in find_intersection"
$ git push origin feature/optimize-intersection
GitHub Actions läuft erneut:
Run tests
============================= test session starts ==============================
collected 47 items
tests/test_geometry.py ...... [ 12%]
tests/test_road.py ......... [ 31%]
tests/test_visualization.py .... [ 40%]
tests/test_integration.py .......... [ 61%]
tests/test_edge_cases.py .................. [100%]
============================= 47 passed in 2.34s ================================
Resultat:
- ✅ Alle Tests bestehen
- ✅ CI ist grün
- ✅ PR kann jetzt gemerged werden
3. Teil 2: Test-Coverage - Welcher Code ist getestet?
3.1. Das Problem: “Alle Tests bestehen” ≠ “Aller Code ist getestet”
Szenario: Ihre CI ist grün. Alle 47 Tests bestehen. Sie fühlen sich sicher.
$ pytest tests/
================================ 47 passed in 2.34s ==================================
Aber dann: Ein Benutzer meldet einen Bug in Produktion:
# Bug Report: Crash when angle > 90 degrees
# Code: geometry.py, line 78
# Error: ValueError: math domain error in tan()
Sie schauen sich den Code an:
def find_intersection(x_road, y_road, angle_degrees, camera_x=0, camera_y=1.5):
"""Find intersection of camera ray with road profile."""
if len(x_road) == 0 or len(y_road) == 0:
return None
# 🐛 BUG: Keine Validierung für angle_degrees > 90
# tan(90°) = undefiniert (vertikaler Strahl)
angle_rad = -np.deg2rad(angle_degrees)
slope = np.tan(angle_rad) # 💥 Crash wenn angle = 90
# ... Rest der Funktion ...
Sie checken Ihre Tests:
# tests/test_geometry.py
def test_find_intersection_normal_angle():
"""Test with normal viewing angle."""
result = find_intersection(x_road, y_road, angle_degrees=-5.0)
assert result is not None
def test_find_intersection_steep_angle():
"""Test with steeper viewing angle."""
result = find_intersection(x_road, y_road, angle_degrees=-15.0)
assert result is not None
def test_find_intersection_shallow_angle():
"""Test with shallow viewing angle."""
result = find_intersection(x_road, y_road, angle_degrees=-2.0)
assert result is not None
Das Problem:
- ✅ Sie haben Tests für
-5°,-15°,-2° - ❌ Sie haben keine Tests für Edge Cases wie
90°,-90°,0° - ❌ Diese Zeile wurde nie ausgeführt von Ihren Tests:
slope = np.tan(angle_rad)
Die Frage:
“Wie viel von meinem Code wird eigentlich von meinen Tests ausgeführt?”
Die Antwort: Test-Coverage
3.2. Was ist Test-Coverage?
Definition: Test-Coverage (oder Code-Coverage) ist ein Maß, das angibt, welcher Prozentsatz Ihres Codes von Ihren Tests ausgeführt wird.
Formel:
\[\text{Coverage} = \frac{\text{Anzahl der ausgeführten Zeilen}}{\text{Gesamtanzahl der Zeilen}} \times 100\%\]Beispiel:
# geometry.py (10 Zeilen Code)
def find_intersection(x_road, y_road, angle_degrees, camera_x=0, camera_y=1.5):
# Zeile 1: if len(x_road) == 0:
if len(x_road) == 0 or len(y_road) == 0:
# Zeile 2: return None
return None
# Zeile 3: angle_rad = ...
angle_rad = -np.deg2rad(angle_degrees)
# Zeile 4: slope = ...
slope = np.tan(angle_rad)
# Zeile 5: intercept = ...
intercept = camera_y - slope * camera_x
# Zeile 6-10: ... Rest der Funktion ...
Wenn Ihre Tests nur Zeilen 1, 3, 5, 6-10 ausführen:
\[\text{Coverage} = \frac{8}{10} \times 100\% = 80\%\]Was bedeutet das:
- ✅ 80% des Codes wurde von Tests ausgeführt
- ❌ 20% des Codes wurde nie getestet (Zeilen 2, 4)
- 🚨 Potentielle Bugs könnten in den ungetesteten 20% versteckt sein
Vertiefung (Kapitel 03 (Testtheorie und Coverage)): In Kapitel 03 (Testtheorie und Coverage) werden wir die theoretischen Grundlagen von Coverage erkunden. Sie werden lernen, dass Coverage immer “Coverage eines Modells” ist - und es gibt verschiedene Modelle (Kontrollfluss-Graphen, Input-Domain-Partitionen), die zu verschiedenen Coverage-Kriterien führen (Statement, Branch, Path, etc.). Verstehen Sie für jetzt einfach, dass Coverage Ihnen sagt “welcher Prozentsatz des Codes während der Tests ausgeführt wurde.”
3.3. Arten von Coverage
Es gibt mehrere Arten von Coverage-Metriken:
1. Line Coverage (Zeilen-Coverage)
Fragt: “Wurde diese Zeile ausgeführt?”
# ✅ Zeile ausgeführt
result = find_intersection(x_road, y_road, angle_degrees=-5.0)
# ❌ Zeile nie ausgeführt
if angle_degrees > 90:
raise ValueError("Angle too large")
Am häufigsten verwendet, einfach zu messen.
2. Branch Coverage (Zweig-Coverage)
Fragt: “Wurden beide Zweige (if/else) ausgeführt?”
if len(x_road) == 0:
return None # ❓ Wurde dieser Zweig getestet?
else:
# Berechnung fortsetzen # ❓ Wurde dieser Zweig getestet?
Beispiel:
# Test 1: Leeres Array
def test_empty_array():
result = find_intersection([], [], -5.0)
assert result is None # ✅ Testet "if"-Zweig
# Test 2: Nicht-leeres Array
def test_normal_array():
result = find_intersection([0, 1], [0, 0], -5.0)
assert result is not None # ✅ Testet "else"-Zweig
Branch Coverage: 100% (beide Zweige getestet)
3. Function Coverage (Funktions-Coverage)
Fragt: “Wurde diese Funktion aufgerufen?”
# ✅ Funktion getestet
def find_intersection(...):
pass
# ❌ Funktion nie aufgerufen
def calculate_curvature(...):
pass
In diesem Kurs: Wir konzentrieren uns auf Line Coverage, da es am einfachsten zu verstehen und zu messen ist.
3.4. Tool: coverage.py und pytest-cov
Python hat zwei beliebte Coverage-Tools:
coverage.py: Low-Level-Tool zum Messen von Coveragepytest-cov: Pytest-Plugin, dascoverage.pyintegriert
Wir verwenden pytest-cov weil:
- ✅ Nahtlose Integration mit pytest
- ✅ Einfache Befehlszeilen-Optionen
- ✅ Schöne Berichte (Terminal, HTML, XML)
3.5. Installation und Verwendung von pytest-cov
Schritt 1: Installiere pytest-cov
# Mit uv (empfohlen für diesen Kurs)
$ uv add --dev pytest-cov
# Mit pip
$ pip install pytest-cov
Schritt 2: Führe Tests mit Coverage aus
$ uv run pytest tests/ --cov=code_examples
Optionen:
--cov=code_examples: Messe Coverage für dascode_examples/Verzeichnis--cov-report=term-missing: Zeige, welche Zeilen vermisst werden--cov-report=html: Generiere HTML-Bericht
Beispielausgabe:
============================= test session starts ==============================
collected 47 items
tests/test_geometry.py ...... [ 12%]
tests/test_road.py ......... [ 31%]
tests/test_visualization.py .... [ 40%]
tests/test_integration.py .......... [ 61%]
tests/test_edge_cases.py .................. [100%]
---------- coverage: platform linux, python 3.12.0 -----------
Name Stmts Miss Cover Missing
---------------------------------------------------------------
code_examples/__init__.py 0 0 100%
code_examples/geometry.py 45 5 89% 78-82
code_examples/road.py 38 2 95% 102, 115
code_examples/visualization.py 52 8 85% 67-74
code_examples/main.py 23 5 78% 45-49
---------------------------------------------------------------
TOTAL 158 20 87%
============================= 47 passed in 2.67s ================================
Was bedeutet das:
| Spalte | Bedeutung |
|---|---|
| Stmts | Anzahl der Code-Zeilen (Statements) |
| Miss | Anzahl der Zeilen, die nie ausgeführt wurden |
| Cover | Coverage-Prozentsatz = (Stmts - Miss) / Stmts × 100% |
| Missing | Zeilennummern, die nicht abgedeckt sind |
Beispiel:
geometry.py: 45 Zeilen, 5 vermisst → 89% Coverage- Vermisste Zeilen: 78-82 (Winkel-Validierung!)
3.6. Beispiel: Coverage-Bericht interpretieren
Szenario: Sie schaust auf den Coverage-Bericht und siehst:
Name Stmts Miss Cover Missing
-----------------------------------------------------
code_examples/geometry.py 45 5 89% 78-82
Schritt 1: Schauen Sie sich die vermissten Zeilen an
# code_examples/geometry.py
def find_intersection(x_road, y_road, angle_degrees, camera_x=0, camera_y=1.5):
"""Find intersection of camera ray with road profile."""
if len(x_road) == 0 or len(y_road) == 0:
return None
# Zeilen 78-82: NIE AUSGEFÜHRT
if abs(angle_degrees) >= 90:
raise ValueError(
f"Invalid angle: {angle_degrees}. "
"Angle must be between -90 and 90 degrees."
)
angle_rad = -np.deg2rad(angle_degrees)
slope = np.tan(angle_rad)
# ... Rest ...
Schritt 2: Verstehen, warum sie vermisst werden
Sie checken Ihre Tests:
# tests/test_geometry.py
# ✅ Getestet: Normale Winkel
def test_find_intersection_normal_angle():
result = find_intersection(x_road, y_road, angle_degrees=-5.0)
assert result is not None
# ❌ NICHT GETESTET: Winkel >= 90 Grad
# (Deswegen Zeilen 78-82 vermisst werden)
Schritt 3: Schreibe einen Test für den vermissten Code
# tests/test_geometry.py
def test_find_intersection_invalid_angle():
"""Test that angles >= 90 degrees raise ValueError."""
x_road = np.array([0, 1, 2])
y_road = np.array([0, 0, 0])
# Test angle = 90 degrees
with pytest.raises(ValueError, match="Invalid angle"):
find_intersection(x_road, y_road, angle_degrees=90)
# Test angle = -90 degrees
with pytest.raises(ValueError, match="Invalid angle"):
find_intersection(x_road, y_road, angle_degrees=-90)
# Test angle > 90 degrees
with pytest.raises(ValueError, match="Invalid angle"):
find_intersection(x_road, y_road, angle_degrees=120)
Schritt 4: Führe Tests erneut mit Coverage aus
$ uv run pytest tests/ --cov=code_examples --cov-report=term-missing
Neue Ausgabe:
Name Stmts Miss Cover Missing
-----------------------------------------------------
code_examples/geometry.py 45 0 100%
Resultat:
- ✅ 100% Coverage auf
geometry.py - ✅ Alle Zeilen wurden ausgeführt
- ✅ Bug abgefangen, bevor er in Produktion geht
3.7. HTML Coverage-Bericht
Für eine bessere Visualisierung können Sie einen HTML-Bericht generieren:
$ uv run pytest tests/ --cov=code_examples --cov-report=html
Das erstellt:
htmlcov/
├── index.html # Hauptbericht
├── geometry_py.html # Detaillierter Bericht für geometry.py
├── road_py.html # Detaillierter Bericht für road.py
└── ...
Öffnen Sie htmlcov/index.html in Ihrem Browser:
# Windows
$ start htmlcov/index.html
# macOS
$ open htmlcov/index.html
# Linux
$ xdg-open htmlcov/index.html
Sie werden sehen:
- 📊 Gesamtübersicht: Coverage für jede Datei
- 📄 Dateidetails: Zeilen farbcodiert (grün = abgedeckt, rot = vermisst)
- 🔍 Klickbar: Klicke auf Dateinamen, um detaillierte Ansicht zu sehen
Beispiel-Screenshot (konzeptuell):
Coverage Report
───────────────────────────────────────────
File Stmts Miss Cover
───────────────────────────────────────────
geometry.py 45 5 89% [View Details]
road.py 38 2 95% [View Details]
visualization.py 52 8 85% [View Details]
───────────────────────────────────────────
TOTAL 158 20 87%
Detaillierte Ansicht (geometry.py):
1 import numpy as np
2
3 def find_intersection(x_road, y_road, angle_degrees, camera_x=0, camera_y=1.5):
4 """Find intersection of camera ray with road profile."""
5
6 if len(x_road) == 0 or len(y_road) == 0: ✅ Abgedeckt
7 return None ✅ Abgedeckt
8
9 if abs(angle_degrees) >= 90: ❌ Nicht abgedeckt (Zeile 78)
10 raise ValueError(...) ❌ Nicht abgedeckt (Zeile 79)
Grün = Von Tests ausgeführt Rot = Nie ausgeführt
4. Teil 3: Test-Coverage zu CI hinzufügen
4.1. Warum Coverage in CI messen?
Problem: Sie führen Coverage lokal aus und siehst 87% Coverage. Gut!
Aber:
- Ihr Teamkollege führt Coverage nie aus
- Ein anderer Entwickler fügt neuen Code hinzu, ohne Tests
- Coverage sinkt auf 65%, aber niemand bemerkt es
Lösung: Automatisiere Coverage-Messung in CI
Vorteile:
- Sichtbarkeit: Jeder PR zeigt Coverage-Änderung
- Rechenschaftspflicht: Entwickler sehen, wenn sie Coverage senken
- Trendanalyse: Coverage-Verlauf im Zeitverlauf verfolgen
- Durchsetzung: Optional Coverage-Mindestanforderungen setzen
4.2. Aktualisieren deines CI-Workflows mit Coverage
Aktualisierte .github/workflows/quality.yml:
name: Code Quality Checks
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
quality:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
version: "latest"
- name: Install dependencies
run: uv sync
- name: Run Ruff linter
run: uv run ruff check code_examples/
- name: Run Ruff formatter
run: uv run ruff format --check code_examples/
- name: Run Pyright
run: uv run pyright code_examples/
# 🆕 AKTUALISIERT: Führe Tests mit Coverage aus
- name: Run tests with coverage
run: |
uv run pytest tests/ \
--cov=code_examples \
--cov-report=term-missing \
--cov-report=html \
--cov-fail-under=70
# 🆕 NEU: Lade Coverage-Bericht als Artefakt hoch
- name: Upload coverage report
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: htmlcov/
Neue Optionen:
--cov-report=html: Generiere HTML-Bericht (für Artefakt)--cov-fail-under=70: Schläge fehl, wenn Coverage < 70%upload-artifact: Mache HTML-Bericht herunterladbar von GitHub
4.3. Coverage-Schwellenwert: –cov-fail-under
Option: --cov-fail-under=70
Was es macht:
- Wenn Coverage < 70% → pytest beendet mit Exit-Code 1 (Fehler)
- Wenn Coverage ≥ 70% → pytest beendet mit Exit-Code 0 (Erfolg)
Beispiel:
# Coverage = 87% → ERFOLG
$ pytest tests/ --cov=code_examples --cov-fail-under=70
...
TOTAL 158 20 87%
============================= 47 passed in 2.67s ================================
$ echo $?
0 # Exit-Code 0 = Erfolg
# Coverage = 65% → FEHLER
$ pytest tests/ --cov=code_examples --cov-fail-under=70
...
TOTAL 158 55 65%
FAIL Required test coverage of 70% not reached. Total coverage: 65.00%
============================= 47 passed in 2.67s ================================
$ echo $?
1 # Exit-Code 1 = Fehler
In CI:
- ✅ Wenn Coverage ≥ 70% → CI-Check besteht
- ❌ Wenn Coverage < 70% → CI-Check schlägt fehl → PR kann nicht gemerged werden
Empfohlene Schwellenwerte:
| Projekt-Typ | Empfohlene Coverage |
|---|---|
| Neue Projekte | 70-80% |
| Kritische Systeme | 80-90% |
| Legacy-Code | 50-60% (schrittweise erhöhen) |
| Libraries | 90%+ |
Für diesen Kurs: 70% ist ein gutes Ziel.
Vorschau (Kapitel 03 (Testtheorie und Coverage)): Das Coverage-Tool meldet “Line Coverage” (auch Statement Coverage oder C0 genannt). In Kapitel 03 (Testtheorie und Coverage) werden Sie “Branch Coverage” (C1) kennenlernen, das stärker ist - es erfordert das Testen sowohl der True- als auch der False-Ergebnisse jeder Entscheidung. Branch Coverage gibt mehr Vertrauen, erfordert aber mehr Tests.
4.4. Coverage-Bericht als Artefakt hochladen
Warum: HTML-Coverage-Berichte sind großartig für die Visualisierung, aber sie sind nicht in CI-Logs sichtbar.
Lösung: Lade den HTML-Bericht als GitHub Actions-Artefakt hoch.
Workflow-Schritt:
- name: Upload coverage report
if: always() # Auch hochladen, wenn Tests fehlschlagen
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: htmlcov/
Zugriff auf den Bericht:
- Gehen Sie zu Ihrem PR auf GitHub
- Scrolle zu Checks → Code Quality Checks
- Klicke auf Summary
- Unter Artifacts → coverage-report → Download
- Entpacke und öffne
htmlcov/index.html
Nutzen:
- Reviewer können Coverage-Details sehen
- Debuggen, warum Coverage gefallen ist
- Identifizieren, welche Zeilen nicht abgedeckt sind
4.5. Beispiel: PR mit Coverage-Bericht
Szenario: Sie erstellen einen PR, der neuen Code hinzufügt, aber keine Tests.
Workflow-Ausgabe:
Run tests with coverage
============================= test session starts ==============================
collected 47 items
tests/test_geometry.py ...... [ 12%]
tests/test_road.py ......... [ 31%]
tests/test_visualization.py .... [ 40%]
tests/test_integration.py .......... [ 61%]
tests/test_edge_cases.py .................. [100%]
---------- coverage: platform linux, python 3.12.0 -----------
Name Stmts Miss Cover Missing
-----------------------------------------------------
code_examples/geometry.py 50 8 84% 78-82, 95-97
code_examples/road.py 38 2 95% 102, 115
-----------------------------------------------------
TOTAL 158 25 84%
FAIL Required test coverage of 70% not reached. Total coverage: 84.00%
Error: Process completed with exit code 1.
Warte, was? Coverage ist 84%, aber es schlägt fehl?
Erklärung:
--cov-fail-under=70würde bestehen (84% > 70%)- Aber pytest schlägt aus einem anderen Grund fehl (z.B. ein Test schlägt fehl)
Richtiges Beispiel (Coverage zu niedrig):
TOTAL 158 55 65%
FAIL Required test coverage of 70% not reached. Total coverage: 65.00%
Resultat:
- ❌ CI schlägt fehl
- ❌ PR kann nicht gemerged werden
- 🔧 Sie müssen Tests hinzufügen, um Coverage zu erhöhen
5. Teil 4: Was Coverage NICHT ist
5.1. Häufiges Missverständnis: “100% Coverage = Fehlerfreier Code”
Mythos:
“Wenn ich 100% Coverage habe, hat mein Code keine Bugs.”
Realität:
“100% Coverage bedeutet, dass alle Zeilen ausgeführt wurden, nicht dass sie korrekt sind.”
Beispiel:
# code_examples/geometry.py
def calculate_distance(x1, y1, x2, y2):
"""Calculate Euclidean distance between two points."""
# 🐛 BUG: Sollte (x2-x1)**2 + (y2-y1)**2 sein, nicht (x2+x1)**2
return ((x2 + x1)**2 + (y2 - y1)**2) ** 0.5
Test:
# tests/test_geometry.py
def test_calculate_distance():
"""Test distance calculation."""
result = calculate_distance(0, 0, 3, 4)
# ❌ FALSCHER TEST: Erwartet falsches Ergebnis
assert result == 5.0 # Zufällig korrekt für (0,0) → (3,4)
Coverage:
$ pytest tests/ --cov=code_examples
Name Stmts Miss Cover
-------------------------------------------
code_examples/geometry.py 3 0 100% ✅ 100% Coverage
Aber der Code hat einen Bug!
# Wenn wir einen anderen Test versuchen:
def test_calculate_distance_different_points():
result = calculate_distance(1, 1, 4, 5)
# Erwartet: sqrt((4-1)^2 + (5-1)^2) = sqrt(9 + 16) = 5.0
# Tatsächlich: sqrt((4+1)^2 + (5-1)^2) = sqrt(25 + 16) = 6.4 ❌
assert result == 5.0 # SCHLÄGT FEHL
Lektion:
- ✅ Coverage sagt Ihnen, ob Code ausgeführt wurde
- ❌ Coverage sagt Ihnen NICHT, ob Code korrekt ist
- 🔑 Sie brauchen immer noch gute Tests (Äquivalenzklassen, Grenzwerte, Assertions)
Vertiefung (Kapitel 03 (Testtheorie und Coverage)): Wir werden diese Einschränkungen in Kapitel 03 (Testtheorie und Coverage) viel detaillierter erkunden, einschließlich spezifischer Beispiele, wie 100% Coverage immer noch Bugs übersehen kann, und wie Coverage mit formalen Anforderungen zusammenhängt.
5.2. Coverage ist ein Werkzeug, kein Ziel
Gefährliche Denkweise:
“Unser Ziel ist 100% Coverage. Schreibe Tests, um diese Zahl zu erreichen.”
Bessere Denkweise:
“Unser Ziel ist zuverlässiger Code. Coverage hilft uns, ungetestete Bereiche zu identifizieren.”
Beispiel schlechter Praxis:
# Nur geschrieben, um Coverage zu erhöhen
def test_useless():
"""Dieser Test erhöht Coverage, überprüft aber nichts."""
result = find_intersection([0, 1], [0, 0], -5.0)
# Keine Assertions! Nur Coverage 📈
Resultat:
- ✅ Coverage steigt
- ❌ Code-Qualität verbessert sich nicht
- ❌ Bugs werden nicht abgefangen
Beispiel guter Praxis:
def test_find_intersection_normal_case():
"""Test that intersection is found for valid inputs."""
x_road = np.array([0, 1, 2, 3, 4])
y_road = np.array([0, 0, 0, 0, 0])
result = find_intersection(x_road, y_road, angle_degrees=-5.0)
# ✅ Klare Assertions
assert result is not None, "Should find intersection"
assert 0 <= result <= 4, "Intersection should be within road bounds"
Resultat:
- ✅ Coverage steigt
- ✅ Code-Korrektheit wird verifiziert
- ✅ Bugs werden abgefangen
5.3. Das Goodhart-Gesetz
Goodhart-Gesetz:
“Wenn ein Maß zu einem Ziel wird, hört es auf, ein gutes Maß zu sein.”
Angewendet auf Coverage:
- Coverage als Maß: Hilft, ungetestete Code-Bereiche zu identifizieren ✅
- Coverage als Ziel: Führt zu sinnlosen Tests, nur um die Zahl zu erhöhen ❌
Anzeichen, dass Sie Coverage als Ziel behandeln:
- 🚩 Tests ohne Assertions (nur um Zeilen auszuführen)
- 🚩 Testen trivialer Code-Pfade (z.B. Getter/Setter)
- 🚩 Fokussierung auf Coverage-Prozentsatz statt Codequalität
- 🚩 Schreiben von Tests, um “100% zu erreichen”, ohne nachzudenken, was getestet werden sollte
Richtiger Ansatz:
- Schreibe sinnvolle Tests basierend auf Anforderungen
- Verwende Coverage, um Lücken zu finden
- Analysiere Lücken: Braucht dieser Code einen Test?
- Füge Tests hinzu, wo sie Wert hinzufügen
- Akzeptiere < 100% wenn es sinnvoll ist
5.4. Wann 100% Coverage NICHT das Ziel ist
Szenarien, in denen < 100% Coverage in Ordnung ist:
1. Trivialer Code
# Getter/Setter - Kein Geschäftslogik
@property
def camera_x(self):
return self._camera_x
@camera_x.setter
def camera_x(self, value):
self._camera_x = value
Abdecken? Vielleicht nicht. Kein Wert.
2. Defensive Fehlerbehandlung
def process_data(data):
try:
return data.process()
except AttributeError: # Sollte nie passieren
# Defensive Programmierung, für den Fall eines unerwarteten Bugs
logger.error("Unexpected: data has no process() method")
return None
Abdecken? Schwer zu testen, niedriger ROI.
3. Legacy-Code
# Alter Code, den niemand versteht, aber der funktioniert
def mysterious_calculation(x):
return ((x * 17) % 42) + (x // 7) * 3
Abdecken? Vielleicht später, wenn Zeit vorhanden ist.
4. Debugging-Code
if DEBUG:
print(f"Debug: x={x}, y={y}, angle={angle}")
Abdecken? Nein, Debug-Modus ist nicht für Tests.
Pragmatischer Ansatz:
- 70-80% Coverage: Gut für die meisten Projekte
- 80-90% Coverage: Gut für kritische Systeme
- 90-100% Coverage: Nur für Kernlogik oder wenn rechtlich/sicherheitstechnisch erforderlich
6. Teil 5: Test-Driven Development (TDD)
6.1. Was ist TDD?
Definition: Test-Driven Development (TDD) ist eine Softwareentwicklungspraxis, bei der Sie Tests schreiben BEVOR Sie Implementierungscode schreiben.
Herkömmlicher Ansatz:
1. Schreibe Code
2. Teste Code (manuell oder mit Tests)
3. Fixe Bugs
4. Wiederhole
TDD-Ansatz:
1. Schreibe einen Test (schlägt fehl)
2. Schreibe gerade genug Code, um den Test zu bestehen
3. Refaktoriere Code
4. Wiederhole
Kernprinzip:
“Sie schreiben keinen Produktionscode, außer um einen fehlgeschlagenen Test zu bestehen.”
6.2. Der Red-Green-Refactor-Zyklus
TDD folgt einem strengen Zyklus:
graph LR
A[🔴 Red<br/>Schreibe fehlgeschlagenen Test] --> B[🟢 Green<br/>Schreibe Code, um zu bestehen]
B --> C[🔵 Refactor<br/>Verbessere Code]
C --> A
Phase 1: 🔴 Red (Rot)
- Schreibe einen Test für eine neue Funktion
- Führe den Test aus → er schlägt fehl (weil die Funktion noch nicht existiert)
- Das ist gut! Es bedeutet, Ihr Test funktioniert.
Phase 2: 🟢 Green (Grün)
- Schreibe das Minimum an Code, um den Test zu bestehen
- Machen Sie sich keine Sorgen um Eleganz, lassen Sie nur den Test bestehen
- Führe den Test aus → er besteht
Phase 3: 🔵 Refactor (Refaktorieren)
- Verbessere den Code (Umbenennung, Extraktion, Vereinfachung)
- Führe Tests aus, um sicherzustellen, dass nichts kaputt ging
- Alle Tests sollten immer noch grün sein
Wiederhole diesen Zyklus für jede neue Funktion.
6.3. TDD-Beispiel: Schreibe validate_angle()
Szenario:
Sie wollen Winkel-Validierung zu geometry.py hinzufügen.
Anforderung:
- Winkel muss zwischen -90 und +90 Grad liegen
- Wirf
ValueErrorfür ungültige Winkel
Traditioneller Ansatz:
# 1. Schreibe Funktion
def validate_angle(angle_degrees):
if abs(angle_degrees) >= 90:
raise ValueError(f"Invalid angle: {angle_degrees}")
# 2. Teste manuell
validate_angle(45) # OK
validate_angle(90) # Sollte werfen... macht es?
TDD-Ansatz: Folge Red-Green-Refactor.
Iteration 1: Validiere normalen Winkel
🔴 RED: Schreibe fehlgeschlagenen Test
# tests/test_geometry.py
import pytest
from code_examples.geometry import validate_angle
def test_validate_angle_normal():
"""Test that normal angles are accepted."""
validate_angle(45) # Sollte nicht werfen
validate_angle(-45) # Sollte nicht werfen
validate_angle(0) # Sollte nicht werfen
Führe Test aus:
$ pytest tests/test_geometry.py::test_validate_angle_normal
...
ImportError: cannot import name 'validate_angle' from 'code_examples.geometry'
FAILED
✅ Gut! Test schlägt fehl (Funktion existiert nicht).
🟢 GREEN: Schreibe minimalen Code
# code_examples/geometry.py
def validate_angle(angle_degrees):
pass # Tut nichts, aber lässt Test bestehen
Führe Test aus:
$ pytest tests/test_geometry.py::test_validate_angle_normal
...
PASSED ✅
✅ Test besteht! (Weil kein raise → keine Ausnahme → Test besteht)
🔵 REFACTOR: Nichts zu refaktorieren (Code ist trivial)
Iteration 2: Validiere ungültige Winkel
🔴 RED: Schreibe fehlgeschlagenen Test
# tests/test_geometry.py
def test_validate_angle_invalid():
"""Test that invalid angles raise ValueError."""
with pytest.raises(ValueError, match="Invalid angle: 90"):
validate_angle(90)
with pytest.raises(ValueError, match="Invalid angle: -90"):
validate_angle(-90)
with pytest.raises(ValueError, match="Invalid angle: 120"):
validate_angle(120)
Führe Test aus:
$ pytest tests/test_geometry.py::test_validate_angle_invalid
...
FAILED (test did not raise ValueError)
✅ Gut! Test schlägt fehl (Funktion wirft nicht).
🟢 GREEN: Schreibe minimalen Code
# code_examples/geometry.py
def validate_angle(angle_degrees):
if abs(angle_degrees) >= 90:
raise ValueError(f"Invalid angle: {angle_degrees}")
Führe Test aus:
$ pytest tests/test_geometry.py::test_validate_angle_invalid
...
PASSED ✅
✅ Test besteht!
🔵 REFACTOR: Code verbessern
# code_examples/geometry.py
def validate_angle(angle_degrees):
"""Validate that angle is within valid range (-90, 90).
Args:
angle_degrees: Viewing angle in degrees
Raises:
ValueError: If angle is outside valid range
"""
if abs(angle_degrees) >= 90:
raise ValueError(
f"Invalid angle: {angle_degrees}. "
"Angle must be between -90 and 90 degrees."
)
Führe alle Tests aus:
$ pytest tests/test_geometry.py
...
PASSED ✅ (beide Tests bestehen)
✅ Refactoring abgeschlossen, Tests sind immer noch grün.
Iteration 3: Validiere Grenzwerte
🔴 RED: Schreibe fehlgeschlagenen Test
# tests/test_geometry.py
def test_validate_angle_boundary():
"""Test boundary values."""
# 89 sollte OK sein
validate_angle(89)
validate_angle(-89)
# 89.9 sollte OK sein
validate_angle(89.9)
validate_angle(-89.9)
# 90.0 sollte werfen
with pytest.raises(ValueError):
validate_angle(90.0)
Führe Test aus:
$ pytest tests/test_geometry.py::test_validate_angle_boundary
...
PASSED ✅
Warte, es hat bestanden?
Ja! Unser bestehender Code behandelt bereits Grenzwerte korrekt.
Das ist TDD in Aktion:
- Wir haben einen Test geschrieben, um uns zu vergewissern
- Er hat bestanden → unser Code ist bereits korrekt
- Wenn er fehlgeschlagen wäre → wir hätten einen Bug gefunden
🔵 REFACTOR: Nichts zu ändern
Code ist bereits sauber. Fertig!
6.4. Vorteile von TDD
1. Design verbessern
- Tests zwingen Sie, über API-Design nachzudenken
- Funktionen werden testbarer (kleinere, fokussierte Funktionen)
- Weniger eng gekoppelter Code
2. Sofortiges Feedback
- Sie wissen innerhalb von Sekunden, ob Ihr Code funktioniert
- Keine manuellen Tests notwendig
3. Bessere Coverage
- Per Definition hat TDD-Code 100% Coverage (jede Zeile wurde geschrieben, um einen Test zu bestehen)
4. Weniger Debugging
- Bugs werden gefangen, sobald sie eingeführt werden
- Keine langen Debugging-Sitzungen später
5. Vertrauen beim Refactoring
- Tests fungieren als Sicherheitsnetz
- Refaktorieren Sie furchtlos, wissend, dass Tests Sie absichern
6. Lebendige Dokumentation
- Tests zeigen, WIE Code verwendet werden soll
- Bessere Dokumentation als Kommentare (die veralten können)
6.5. Herausforderungen von TDD
1. Anfängliche Lernkurve
- Fühlt sich langsam an am Anfang
- Erfordert Denken “Test-first”
- Bedarf Übung, um natürlich zu werden
2. Zeitaufwand
- Mehr Code zu schreiben (Tests + Implementierung)
- Kann sich verlangsamen für einfache Features
3. Overhead für prototyping
- Wenn Sie explorieren, kann TDD zu starr sein
- Manchmal wollen Sie nur schnell experimentieren
4. Schwierig für Legacy-Code
- Wenn Code nicht testbar ist, ist TDD schwer
- Könnte Refactoring erfordern, bevor TDD angewendet werden kann
Pragmatische Lösung:
- Verwende TDD für kritische Logik (Kernalgorithmen, Geschäftslogik)
- Überspringe TDD für triviale Code (Boilerplate, einfache Getter/Setter)
- Verwende TDD für Bug-Fixes (schreibe Test, der Bug reproduziert, dann fixe)
6.6. TDD in der Praxis: Wann es zu verwenden ist
Verwende TDD:
- ✅ Für neue Features mit klaren Anforderungen
- ✅ Für Bug-Fixes (schreibe Test, der Bug reproduziert)
- ✅ Für kritische Logik (Algorithmen, Berechnungen)
- ✅ Für öffentliche APIs (sicherstellen, dass Vertrag erfüllt ist)
Überspringe TDD:
- ❌ Für schnelles Prototyping (Sie erkunden noch)
- ❌ Für triviale Code (einfache Getter/Setter)
- ❌ Für UI-Code (schwierig zu testen, visuelles Feedback benötigt)
- ❌ Für Experimente (Sie wissen noch nicht, was Sie bauen wollen)
Hybridansatz (empfohlen):
1. Explorative Phase: Schreibe Code ohne Tests (schnell experimentieren)
2. Stabilisierungsphase: Sobald klar, was funktioniert → schreibe Tests
3. Feature-Phase: Verwende TDD für neue Features
4. Wartungsphase: Verwende TDD für Bug-Fixes
Verbindung zu Kapitel 03 (Testtheorie und Coverage): TDD tendiert natürlich dazu, hohe Coverage zu produzieren, weil jede Codezeile geschrieben wird, um einen Test zu bestehen. In Kapitel 03 (Testtheorie und Coverage) werden Sie die formale Beziehung lernen: TDD produziert Test-Suites, die für mehrere Coverage-Kriterien gleichzeitig “adäquat” sind.
7. Teil 6: TDD + Coverage + CI zusammenführen
7.1. Der komplette professionelle Workflow
Bringen wir alles zusammen:
graph TD
A[Neue Feature-Anforderung] --> B{TDD verwenden?}
B -->|Ja - Kritische Logik| C[🔴 RED: Schreibe fehlgeschlagenen Test]
B -->|Nein - Einfaches Feature| D[Schreibe Code]
C --> E[🟢 GREEN: Schreibe minimalen Code]
E --> F[🔵 REFACTOR: Verbessere Code]
F --> G[Führe alle Tests lokal aus]
D --> H[Schreibe Tests nach]
H --> G
G --> I[Prüfe Coverage lokal]
I --> J{Coverage >= 70%?}
J -->|Nein| K[Füge mehr Tests hinzu]
K --> G
J -->|Ja| L[Commit + Push]
L --> M[GitHub Actions CI läuft]
M --> N{Alle Checks bestehen?}
N -->|Nein| O[Checke CI-Logs]
O --> P{Was ist fehlgeschlagen?}
P -->|Tests| Q[Fixe Tests]
P -->|Coverage| R[Füge Tests hinzu]
P -->|Linter| S[Fixe Stil]
Q --> L
R --> L
S --> L
N -->|Ja| T[Erstelle PR]
T --> U[Code Review]
U --> V{Genehmigt?}
V -->|Nein| W[Adressiere Feedback]
W --> L
V -->|Ja| X[Merge zu main]
X --> Y[Feature abgeschlossen! 🎉]
7.2. Hands-On-Beispiel: Kompletter Workflow
Szenario: Sie willst eine neue Funktion hinzufügen: calculate_curvature().
Anforderung:
- Berechne Straßenkrümmung an einem gegebenen Punkt
- Input:
x_road,y_road,index - Output: Krümmung (zweite Ableitung)
Schritt 1: Feature-Branch erstellen
$ git checkout main
$ git pull origin main
$ git checkout -b feature/add-curvature-calculation
Schritt 2: TDD - 🔴 RED (Schreibe fehlgeschlagenen Test)
# tests/test_road.py
import numpy as np
from code_examples.road import calculate_curvature
def test_calculate_curvature_straight_road():
"""Test curvature of straight road is zero."""
x_road = np.array([0, 1, 2, 3, 4])
y_road = np.array([0, 0, 0, 0, 0]) # Gerade Linie
curvature = calculate_curvature(x_road, y_road, index=2)
assert abs(curvature) < 0.01, "Straight road should have zero curvature"
def test_calculate_curvature_curved_road():
"""Test curvature of curved road is non-zero."""
x_road = np.array([0, 1, 2, 3, 4])
y_road = np.array([0, 1, 0, -1, 0]) # Gebogene Linie
curvature = calculate_curvature(x_road, y_road, index=2)
assert curvature != 0, "Curved road should have non-zero curvature"
Führe Tests aus:
$ uv run pytest tests/test_road.py::test_calculate_curvature_straight_road
...
ImportError: cannot import name 'calculate_curvature' from 'code_examples.road'
FAILED ❌
✅ Gut! Test schlägt fehl.
Schritt 3: TDD - 🟢 GREEN (Schreibe minimalen Code)
# code_examples/road.py
import numpy as np
def calculate_curvature(x_road, y_road, index):
"""Calculate road curvature at given index using finite differences."""
if index < 1 or index >= len(x_road) - 1:
return 0.0 # Kann Krümmung nicht an Endpunkten berechnen
# Zweite Ableitung über finite Differenzen
dx = x_road[index + 1] - x_road[index - 1]
dy = y_road[index + 1] - y_road[index - 1]
ddx = x_road[index + 1] - 2 * x_road[index] + x_road[index - 1]
ddy = y_road[index + 1] - 2 * y_road[index] + y_road[index - 1]
# Krümmung = |x'y'' - y'x''| / (x'^2 + y'^2)^(3/2)
numerator = abs(dx * ddy - dy * ddx)
denominator = (dx**2 + dy**2)**1.5
if denominator < 1e-10:
return 0.0
return numerator / denominator
Führe Tests aus:
$ uv run pytest tests/test_road.py::test_calculate_curvature_straight_road
...
PASSED ✅
$ uv run pytest tests/test_road.py::test_calculate_curvature_curved_road
...
PASSED ✅
✅ Tests bestehen!
Schritt 4: TDD - 🔵 REFACTOR (Verbessere Code)
Code sieht gut aus, aber fügen wir Docstring hinzu:
def calculate_curvature(x_road, y_road, index):
"""Calculate road curvature at given index.
Uses finite difference approximation of second derivative.
Args:
x_road: Array of x-coordinates
y_road: Array of y-coordinates
index: Index at which to calculate curvature
Returns:
Curvature value (0 for straight road, > 0 for curves)
Note:
Returns 0 for edge points (index 0 or len-1)
"""
# ... Rest des Codes ...
Führe alle Tests aus:
$ uv run pytest tests/
...
47 passed ✅
✅ Refactoring abgeschlossen, alle Tests grün.
Schritt 5: Prüfe Coverage lokal
$ uv run pytest tests/ --cov=code_examples --cov-report=term-missing
Ausgabe:
Name Stmts Miss Cover Missing
-----------------------------------------------------
code_examples/road.py 42 0 100%
-----------------------------------------------------
TOTAL 158 0 100%
✅ 100% Coverage!
Schritt 6: Commit und Push
$ git add code_examples/road.py tests/test_road.py
$ git commit -m "Add calculate_curvature() with tests"
$ git push origin feature/add-curvature-calculation
Schritt 7: Beobachte GitHub Actions CI
Workflow läuft automatisch:
Code Quality Checks
├─ Checkout code ✅ PASSED
├─ Set up Python ✅ PASSED
├─ Install uv ✅ PASSED
├─ Install dependencies ✅ PASSED
├─ Run Ruff linter ✅ PASSED
├─ Run Ruff formatter ✅ PASSED
├─ Run Pyright ✅ PASSED
└─ Run tests with coverage ✅ PASSED
├─ 49 tests passed
└─ Coverage: 100%
✅ Alle Checks grün!
Schritt 8: Erstelle Pull Request
Auf GitHub:
- Gehen Sie zu Ihrem Repository
- Klicke “Pull requests” → “New pull request”
- Wähle
feature/add-curvature-calculation→main - Titel: “Add road curvature calculation”
- Beschreibung:
## Summary
Adds `calculate_curvature()` function to calculate road curvature at a given point.
## Changes
- Added `calculate_curvature()` to `road.py`
- Added unit tests for straight and curved roads
- Coverage: 100%
## Testing
- ✅ All tests pass (49 total)
- ✅ Coverage: 100%
- ✅ Linter: No issues
- Klicke “Create pull request”
Schritt 9: Code Review
Ihr Teamkollege reviewed:
✅ Sieht gut aus! Tests sind gründlich.
✅ Coverage ist ausgezeichnet.
💡 Eine Frage: Was passiert wenn x_road und y_road unterschiedliche Längen haben?
Sie antworten:
# Füge Validierung hinzu
def calculate_curvature(x_road, y_road, index):
"""..."""
# Neu: Validiere Eingabe
if len(x_road) != len(y_road):
raise ValueError("x_road and y_road must have same length")
if index < 1 or index >= len(x_road) - 1:
return 0.0
# ... Rest des Codes ...
Füge Test hinzu:
def test_calculate_curvature_mismatched_lengths():
"""Test that mismatched array lengths raise ValueError."""
x_road = np.array([0, 1, 2])
y_road = np.array([0, 1]) # Kürzeres Array
with pytest.raises(ValueError, match="same length"):
calculate_curvature(x_road, y_road, index=1)
Commit und Push:
$ git add code_examples/road.py tests/test_road.py
$ git commit -m "Add validation for mismatched array lengths"
$ git push origin feature/add-curvature-calculation
CI läuft erneut → ✅ Grün
Schritt 10: Merge
Reviewer genehmigt:
✅ Approved! Bereit zum Merge.
Sie klicken “Merge pull request”
GitHub Actions läuft auf main:
Code Quality Checks (main branch)
└─ All checks passed ✅
✅ Feature abgeschlossen!
8. Best Practices und Takeaways
8.1. Best Practices für Tests in CI
1. Mache Tests obligatorisch
- ✅ Verwende Branch Protection Rules
- ✅ Erlaube kein Merging, wenn Tests fehlschlagen
2. Halte Tests schnell
- ⚡ CI sollte < 5 Minuten laufen
- ⚡ Langsame Tests → Entwickler umgehen sie
3. Führe Tests bei jedem Push aus
- ✅
on: pull_requestUNDon: push - ✅ Fange Probleme früh
4. Zeige Coverage-Trends
- 📊 Verwende Coverage-Badges (optional)
- 📊 Tracke Coverage im Zeitverlauf
5. Schläge nicht bei kleinen Coverage-Rückgängen fehl
- ⚠️ Schwellenwert: 70% (nicht 100%)
- ⚠️ Erlaube Flexibilität für neue Features
8.2. Best Practices für Coverage
1. Setze realistische Ziele
- 🎯 70-80% für die meisten Projekte
- 🎯 80-90% für kritische Systeme
- 🎯 Nicht 100% anstreben (Goodhart-Gesetz)
2. Fokussiere auf Qualität, nicht Quantität
- ✅ Sinnvolle Tests > hohe Coverage
- ❌ Vermeide Tests ohne Assertions
3. Verwende Coverage, um Lücken zu finden
- 🔍 Schauen Sie sich den HTML-Bericht an
- 🔍 Priorisiere kritische vermisste Zeilen
4. Dokumentiere warum Coverage < 100% ist
- 📝 Kommentar in Code:
# Nicht abgedeckt: Defensive Fehlerbehandlung - 📝 Teamverständnis: Warum ist das OK?
8.3. Best Practices für TDD
1. Starte klein
- 🌱 Schreibe den einfachsten Test zuerst
- 🌱 Füge schrittweise Komplexität hinzu
2. Ein Test auf einmal
- 🔴 Schreibe einen Test → 🟢 Lass ihn bestehen → 🔵 Refaktoriere
- 🔁 Wiederhole
3. Halte Tests isoliert
- ✅ Jeder Test sollte unabhängig laufen
- ❌ Vermeide Abhängigkeiten zwischen Tests
4. Verwende beschreibende Test-Namen
- ✅
test_find_intersection_empty_arrays - ❌
test_function1
5. Teste Verhalten, nicht Implementierung
- ✅ Teste, WAS die Funktion tut
- ❌ Teste nicht, WIE sie es tut
8.4. Was Sie in dieser Vorlesung gelernt haben
1. Tests + CI
- ✅ Wie man pytest zu GitHub Actions hinzufügt
- ✅ Warum Tests in CI obligatorisch sein müssen
- ✅ Branch Protection Rules
2. Test-Coverage
- ✅ Was Coverage ist (und was nicht)
- ✅
coverage.pyundpytest-cov - ✅ Coverage-Berichte interpretieren
- ✅ Coverage-Schwellenwerte setzen (
--cov-fail-under)
3. Test-Driven Development (TDD)
- ✅ Red-Green-Refactor-Zyklus
- ✅ Wann TDD zu verwenden ist (und wann nicht)
- ✅ Vorteile und Herausforderungen
4. Professioneller Workflow
- ✅ TDD + Coverage + CI zusammen
- ✅ Feature-Branch → Tests → Coverage → CI → PR → Merge
- ✅ Vollständiger End-to-End-Workflow
8.5. Übung: Wende es auf Road Profile Viewer an
Ihre Aufgabe:
- Aktiviere Coverage in CI
- Aktualisiere
.github/workflows/quality.yml - Füge
--cov=code_examples --cov-fail-under=70hinzu
- Aktualisiere
- Prüfe aktuelle Coverage
- Führe
pytest --cov=code_examples --cov-report=htmlaus - Öffne
htmlcov/index.html - Identifiziere vermisste Zeilen
- Führe
- Füge Tests für vermisste Zeilen hinzu
- Wähle eine Datei mit < 80% Coverage
- Schreibe Tests, um vermisste Zeilen abzudecken
- Versuche TDD für eine neue Funktion
- Wähle ein kleines Feature (z.B.
validate_road_data()) - Folge Red-Green-Refactor
- Committe nach jedem Zyklus
- Wähle ein kleines Feature (z.B.
- Erstelle einen PR
- Pushen Sie Ihren Branch
- Beobachte CI laufen
- Sehe Coverage in GitHub Actions
9. Zusammenfassung
Was Sie gelernt haben:
- Tests in CI sind obligatorisch
- Tests lokal laufen lassen ≠ Tests werden immer ausgeführt
- GitHub Actions + Branch Protection Rules erzwingen Tests
- Coverage ist ein Werkzeug, kein Ziel
- Misst, welcher Code von Tests ausgeführt wird
- 70-80% Coverage ist ein gutes Ziel für die meisten Projekte
- 100% Coverage ≠ fehlerfreier Code
- TDD ist eine hilfreiche Disziplin
- Red-Green-Refactor-Zyklus
- Schreibe Tests zuerst, Code danach
- Optional, aber leistungsstark für kritische Logik
- Der komplette Workflow
- Feature-Branch → TDD → Coverage → CI → PR → Review → Merge
- Tests, Coverage und CI arbeiten zusammen, um Qualität sicherzustellen
- Automatisierung gibt Ihnen Vertrauen, schnell zu shippen
Nächste Schritte:
- ✅ Aktivieren Sie Coverage in Ihrem CI-Workflow
- ✅ Setze einen realistischen Coverage-Schwellenwert (70%+)
- ✅ Versuchen Sie TDD für Ihr nächstes Feature
- ✅ Verwende Coverage, um Testlücken zu finden
- ✅ Halte Tests schnell und fokussiert
Sie haben jetzt:
- ✅ CI, die Stil, Typen UND Korrektheit prüft
- ✅ Messbare Testabdeckung
- ✅ Einen professionellen Entwicklungsworkflow
Glückwunsch! 🎉 Sie haben die volle CI/CD + Testing Pipeline gemeistert!
10. Weitere Lektüre
Coverage:
TDD:
- Test-Driven Development by Example - Kent Beck
- TDD by Example (Video)
GitHub Actions:
Best Practices:
Kommt als Nächstes: Kapitel 03 (Testtheorie und Coverage) - Testing-Theorie
In Kapitel 03 (Testtheorie und Coverage) tauchen wir tief in die theoretischen Grundlagen ein:
- Formale Definitionen: Program, Input Domain, Test Suite, Model, Coverage Criterion
- Statement Coverage (C0) vs Branch Coverage (C1) - und warum C1 C0 subsumiert
- Wie man einen Kontrollfluss-Graphen liest und Tests systematisch entwirft
- Äquivalenzklassen und Grenzwerte als Coverage-Kriterien
- Die Anforderungen-Tests-Coverage-Spirale: wie Testen fehlende Spezifikationen aufdeckt
Diese Theorie wird Ihnen helfen zu verstehen, warum die praktischen Techniken, die Sie heute gelernt haben, tatsächlich funktionieren.