03 Grundlagen des Testens: Basics
November 2025 (4599 Words, 26 Minutes)
1. Einführung: Ihre CI ist grün, aber funktioniert Ihr Code?
Nach Kapitel 02 (Refactoring) hat Ihr Road Profile Viewer eine schöne modulare Struktur:
src/road_profile_viewer/
├── __init__.py
├── geometry.py # Reine Mathe-Funktionen
├── road.py # Straßengenerierung
├── visualization.py # Dash UI
└── main.py # Einstiegspunkt
Ihre CI-Pipeline validiert jeden Pull Request:
✅ Ruff check (PEP8-Stil)
✅ Ruff format check
✅ Pyright (Type Hints)
Alle Checks grün. Code gemerged. Deployed in Produktion.
Dann passiert das:
# Ein Benutzer gibt einen vertikalen Kamerawinkel ein
angle = 90.0
# Ihr Code stürzt ab:
def find_intersection(x_road, y_road, angle_degrees, ...):
angle_rad = -np.deg2rad(angle_degrees)
slope = np.tan(angle_rad) # tan(90°) = ∞
# ZeroDivisionError or infinite loop!
Ihre CI sagte, nichts sei falsch. Weil:
- ✅ Ruff: “Code ist schön formatiert!” (ist er)
- ✅ Pyright: “Typen sehen gut aus!” (tun sie)
- ❌ Niemand hat getestet, ob der Code tatsächlich funktioniert
Das ist die Lücke, die wir heute füllen.
2. Lernziele
Am Ende dieser Vorlesung werden Sie:
- Den Unterschied verstehen zwischen Code-Qualität (Stil) und Code-Korrektheit (Logik)
- Die Testing-Pyramide meistern - Unit-, Modul- und End-to-End-Tests
- Effektive Unit-Tests schreiben mit Äquivalenzklassen und Boundary-Analyse
- Pytest verwenden, um Ihren modularen Python-Code zu testen
- LLMs nutzen, um Test-Schreiben zu beschleunigen (während Sie ihre Grenzen verstehen)
- Das “Test-Kegel”-Muster brechen, das Entwickler vom Testen abhält
- Feature-Branch-Workflow anwenden, um Tests zu Ihrem Projekt hinzuzufügen
Was Sie lernen WERDEN:
- Wie man Bugs fängt, bevor Benutzer es tun
- Wie man Tests schreibt, die Ihnen Vertrauen geben
- Wie man LLMs als Test-Schreib-Assistenten nutzt (nicht Autopiloten)
- Warum die Testing-Pyramide wichtig ist
Was Sie NICHT (noch) lernen werden:
- Test-Driven Development (TDD) - das ist Kapitel 03 (TDD und CI)
- Mocking und Fixtures - fortgeschrittene Themen für später
- UI-Testing - komplex und außerhalb des Rahmens
3. Teil 1: Warum Testing wichtig ist
3.1 Der Bug, den CI übersehen hat
Schauen wir uns einen echten Bug in Ihrem src/road_profile_viewer/geometry.py an:
from numpy.typing import NDArray
import numpy as np
def find_intersection(
x_road: NDArray[np.float64],
y_road: NDArray[np.float64],
angle_degrees: float,
camera_x: float = 0,
camera_y: float = 1.5,
) -> tuple[float | None, float | None, float | None]:
"""Find intersection between camera ray and road profile."""
angle_rad = -np.deg2rad(angle_degrees)
# Handle vertical ray
if np.abs(np.cos(angle_rad)) < 1e-10:
return None, None, None # Currently returns None for vertical angles
slope = np.tan(angle_rad)
# ... rest of function
Was passiert mit verschiedenen Winkeln?
| Winkel | Erwartetes Verhalten | Tatsächliches Verhalten | CI-Status |
|---|---|---|---|
| -10° | Finde Schnittpunkt | ✅ Funktioniert | ✅ Grün |
| -45° | Finde Schnittpunkt | ✅ Funktioniert | ✅ Grün |
| 0° | Horizontaler Strahl | ✅ Funktioniert | ✅ Grün |
| 90° | Vertikaler Strahl | ❌ Gibt None zurück (sollte elegant behandeln) | ✅ Grün |
| Empty array | Leere Daten behandeln | ❌ Stürzt mit IndexError ab | ✅ Grün |
CI ist blind für Logik-Bugs. Es prüft nur:
- Ist der Code formatiert? (Ruff)
- Sind Typen korrekt? (Pyright)
Es prüft nicht:
- Gibt die Funktion korrekte Ergebnisse zurück?
- Behandelt sie Randfälle?
- Scheitert sie elegant bei ungültiger Eingabe?
3.2 Code-Qualität ≠ Code-Korrektheit
Code-Qualität-Tools (Kapitel 02 (Code-Qualität in der Praxis))
- Was sie prüfen: Stil, Formatierung, Type Hints
- Was sie fangen: PEP8-Verstöße, fehlende Type Hints
- Was sie verpassen: Logik-Fehler, Randfallbugs, falsche Berechnungen
Tests (Diese Vorlesung)
- Was sie prüfen: Funktionalität, Korrektheit, Verhalten
- Was sie fangen: Logik-Bugs, Randfallausfälle, falsche Ergebnisse
- Was sie verpassen: Stilprobleme (dafür ist Ruff da!)
Sie brauchen BEIDES:
Code-Qualität-Tools → "Sieht das professionell aus?"
Tests → "Funktioniert das tatsächlich?"
4. Teil 2: Die Testing-Pyramide
4.1 Was ist die Testing-Pyramide?
Die Testing-Pyramide ist eine Strategie zum Ausgleichen verschiedener Testtypen:
/\
/E2E\ ← Wenige, langsam, teuer
/------\ (Volle Dash-App, Browser-Simulation)
/Module \ ← Einige, moderate Geschwindigkeit
/----------\ (Mehrere Module zusammenarbeiten)
/ Unit \ ← Viele, schnell, günstig
/--------------\ (Einzelne Funktionen)
Drei Ebenen des Testens:
- Unit-Tests (Basis der Pyramide)
- Testen einzelne Funktionen isoliert
- Schnell (Millisekunden)
- Einfach zu schreiben und zu warten
- Fangen Bugs früh
- Modul-/Integrationstests (Mitte der Pyramide)
- Testen mehrere Module, die zusammenarbeiten
- Moderate Geschwindigkeit (Sekunden)
- Fangen Schnittstellendiskrepanzen
- End-to-End-Tests (Spitze der Pyramide)
- Testen den gesamten Anwendungsfluss
- Langsam (Sekunden bis Minuten)
- Teuer zu warten
- Fangen Probleme auf Systemebene
4.2 Warum die Pyramidenform?
Verhältnis-Empfehlung:
- 70% Unit-Tests
- 20% Modul-/Integrationstests
- 10% End-to-End-Tests
Warum?
Geschwindigkeit zählt für die Feedback-Schleife:
Unit-Test: find_intersection()-Test läuft in 0.01 Sekunden
Modul-Test: Geometrie + Straße zusammen läuft in 0.1 Sekunden
E2E-Test: Dash-App starten, Klicks simulieren läuft in 5+ Sekunden
Wenn Sie eine Zeile in find_intersection() ändern:
- ✅ 50 Unit-Tests laufen in 0.5 Sekunden → sofortiges Feedback
- ❌ 50 E2E-Tests laufen in 250 Sekunden → Sie holen Kaffee, verlieren den Flow
Die Feedback-Schleife ist kritisch:
Schnelle Tests → Oft ausführen → Bugs sofort fangen → Schnell reparieren
Langsame Tests → Selten ausführen → Bugs häufen sich → Schwer zu debuggen
4.3 Das Anti-Pattern: Der Test-Kegel (Umgekehrte Pyramide)
Was passiert, wenn Entwickler Unit-Tests vermeiden:
/--------------\
\ E2E / ← Viele E2E-Tests (langsam!)
\----------/
\ Module / ← Einige Modul-Tests
\------/
\ Unit/ ← Wenige oder keine Unit-Tests
\ /
Das ist rückwärts! Probleme:
- Langsames Feedback: Jede Änderung erfordert langsame E2E-Tests
- Schwer zu debuggen: E2E-Test schlägt fehl → welches von 10 Modulen hat es verursacht?
- Spröde: UI-Änderungen brechen alle E2E-Tests
- Entwickler vermeiden Tests: Zu langsam → Bugs häufen sich
Warum entsteht der Kegel?
- “Ich weiß nicht, wie man Unit-Tests schreibt”
- “Test-Setup zu schreiben ist mühsam”
- “Die UI ist einfach manuell durchzuklicken”
- Ergebnis: Nur E2E-Tests, Testing-Pyramide umgekehrt
Heute werden wir das beheben indem wir:
- Unit-Testing richtig lernen
- LLMs nutzen, um mühsames Boilerplate zu handhaben
- Unit-Testing so einfach machen, dass Sie es bevorzugen
5. Teil 3: Unit-Testing Deep Dive
5.1 Was ist ein Unit-Test?
Definition: Ein Unit-Test überprüft, dass eine einzelne Einheit (Funktion, Methode, Klasse) korrekt isoliert funktioniert.
Beispiel:
# Zu testende Einheit: find_intersection()
def test_find_intersection_normal_angle():
"""Test that find_intersection() works with a normal downward angle."""
# Arrange: Testdaten aufsetzen
x_road = np.array([0, 10, 20, 30])
y_road = np.array([0, 2, 3, 4])
angle = -10.0
# Act: Funktion aufrufen
x, y, dist = find_intersection(x_road, y_road, angle)
# Assert: Ergebnis überprüfen
assert x is not None, "Should find an intersection"
assert dist > 0, "Distance should be positive"
Schlüssel-Charakteristiken:
- ✅ Testet EINE Funktion (
find_intersection) - ✅ Keine Abhängigkeiten von Dash, UI oder anderen Modulen
- ✅ Schnell (läuft in Millisekunden)
- ✅ Klar was getestet wird
5.2 Das AAA-Muster: Arrange-Act-Assert
Jeder gute Unit-Test folgt dieser Struktur:
def test_example():
# ARRANGE: Testdaten und Bedingungen aufsetzen
input_data = prepare_test_data()
expected_result = calculate_expected()
# ACT: Die zu testende Funktion aufrufen
actual_result = function_under_test(input_data)
# ASSERT: Überprüfen, dass das Ergebnis den Erwartungen entspricht
assert actual_result == expected_result
Warum dieses Muster?
- Klare Struktur macht Tests lesbar
- Einfach zu sehen, was getestet wird
- Einfach zu debuggen, wenn Tests fehlschlagen
5.3 Die Unmöglichkeit des erschöpfenden Testens
Frage: Warum können wir nicht einfach jede mögliche Eingabe testen?
Machen wir die Mathematik für find_intersection():
def find_intersection(x_road, y_road, angle_degrees, camera_x=0, camera_y=1.5):
"""Find intersection between camera ray and road profile."""
# ...
Nur ein Parameter: angle_degrees (ein Float)
- Float-Präzision: 32-Bit Float hat ~7 signifikante Stellen
- Bereich: -180° bis 180° (oder -90° bis 90° für praktische Winkel)
- Mögliche Werte: Seien wir konservativ und sagen 1.000.000 unterschiedliche Werte
Zeit pro Test: ~1 Millisekunde (sehr schnell!)
Gesamtzeit um nur angle_degrees zu testen:
1.000.000 Tests × 0.001 Sekunden = 1.000 Sekunden ≈ 17 Minuten
Aber warte, wir haben 4 Parameter:
angle_degrees: ~1.000.000 Wertecamera_x: ~1.000.000 Wertecamera_y: ~1.000.000 Wertex_road,y_road: Arrays mit variabler Länge und Werten → effektiv unendlich
Wenn wir ALLE Kombinationen testen (nur für 3 Float-Parameter):
1.000.000³ = 1.000.000.000.000.000.000 Tests (1 Quintillion!)
Bei 1ms pro Test:
= 1.000.000.000.000.000 Sekunden
= 31.709.791.983 Jahre
≈ 32 MILLIARDEN JAHRE
Zur Perspektive:
- Alter des Universums: ~14 Milliarden Jahre
- Zeit um alle Eingaben zu testen: ~32 Milliarden Jahre
- Schlussfolgerung: Erschöpfendes Testen ist buchstäblich unmöglich
Und das ist für EINE Funktion mit einfachen Parametern!
5.4 Testing-Frameworks: Warum wir pytest brauchen
Frage: Können wir nicht einfach Tests als reguläre Python-Funktionen schreiben?
Ja, aber es wird schnell unübersichtlich:
# Ohne Testing-Framework:
def manual_test():
x, y, dist = find_intersection(x_road, y_road, -10.0)
if x is None:
print("❌ FEHLGESCHLAGEN: x sollte nicht None sein")
return False
if dist <= 0:
print("❌ FEHLGESCHLAGEN: Distanz sollte positiv sein")
return False
print("✅ BESTANDEN")
return True
# Test ausführen
manual_test()
Probleme mit manuellem Testen:
- ❌ Keine automatische Test-Erkennung (Sie müssen jede Funktion manuell aufrufen)
- ❌ Keine organisierten Test-Reports (nur Prints)
- ❌ Keine Test-Isolation (ein Fehler könnte andere beeinflussen)
- ❌ Keine hilfreichen Fehlermeldungen (Sie müssen alle Checks schreiben)
- ❌ Keine Test-Fixtures (Setup/Teardown)
- ❌ Keine parallele Ausführung (langsam!)
- ❌ Keine Integration mit CI/CD
Hier kommt pytest: Das Industrie-Standard-Testing-Framework
Was pytest bietet:
# Automatische Test-Erkennung - findet alle test_*.py-Dateien
$ uv run pytest
# Mit detaillierter Ausgabe ausführen
$ uv run pytest -v
# Spezifische Test-Datei ausführen
$ uv run pytest tests/test_geometry.py
# Tests ausführen, die ein Muster matchen
$ uv run pytest -k "intersection"
# Print-Statements auch bei Bestehen zeigen
$ uv run pytest -s
# Bei erstem Fehler stoppen
$ uv run pytest -x
Was pytest großartig macht:
- ✅ Einfache Syntax: Nutze einfach
assert(keine speziellen Methoden) - ✅ Auto-Discovery: Findet automatisch alle
test_*.py-Dateien - ✅ Reiche Ausgabe: Zeigt genau was fehlgeschlagen ist und warum
- ✅ Fixtures: Teile Setup-Code zwischen Tests
- ✅ Parametrisierung: Führe denselben Test mit verschiedenen Eingaben aus
- ✅ Plugins: Erweitere Funktionalität (Coverage, Benchmarking, etc.)
- ✅ CI/CD-Integration: Funktioniert nahtlos mit GitHub Actions
Vergleich:
# Manuelles Testen (primitiv)
def test_something():
result = function()
if result != expected:
print("FEHLGESCHLAGEN")
return False
return True
# pytest (modern)
def test_something():
result = function()
assert result == expected # pytest kümmert sich um den Rest!
pytest-Ausgabe-Beispiel:
$ uv run pytest tests/test_geometry.py -v
============================= test session starts ==============================
platform win32 -- Python 3.12.0, pytest-8.0.0, pluggy-1.4.0
cachedir: .pytest_cache
rootdir: C:\...\road-profile-viewer
collected 5 items
tests/test_geometry.py::test_normal_angle PASSED [ 20%]
tests/test_geometry.py::test_vertical_angle PASSED [ 40%]
tests/test_geometry.py::test_empty_array FAILED [ 60%]
tests/test_geometry.py::test_boundary_angle PASSED [ 80%]
tests/test_geometry.py::test_upward_angle PASSED [100%]
=================================== FAILURES ===================================
________________________________ test_empty_array ______________________________
def test_empty_array():
x_road = np.array([])
y_road = np.array([])
> x, y, dist = find_intersection(x_road, y_road, -10.0)
E IndexError: index 0 is out of bounds for axis 0 with size 0
tests/test_geometry.py:42: IndexError
========================= 1 failed, 4 passed in 0.12s =========================
Beachte:
- ✅ Zeigt genau welcher Test fehlgeschlagen ist
- ✅ Zeigt die genaue Zeile, die fehlgeschlagen ist
- ✅ Zeigt Fehlertyp und Meldung
- ✅ Zeigt wie viele Tests bestanden/fehlgeschlagen sind
- ✅ Zeigt Ausführungszeit
- ✅ Farbcodierte Ausgabe (rot für Fehler, grün für Erfolge)
Warum das wichtig ist:
Ohne pytest: "Etwas ist kaputt. Viel Glück beim Finden!"
Mit pytest: "test_empty_array fehlgeschlagen in Zeile 42 mit IndexError"
Installation (bereits erledigt, wenn Sie uv haben):
# pytest ist typischerweise in Ihrer pyproject.toml aufgelistet
$ uv add --dev pytest
# Tests ausführen
$ uv run pytest
Jetzt, wo wir verstehen WARUM pytest existiert, lassen Sie uns unseren ersten Test schreiben!
5.5 Hands-On: Ihren ersten Unit-Test schreiben
Ziel: Teste die find_intersection()-Funktion.
Schritt 1: Test-Dateistruktur erstellen
# In Ihrem road-profile-viewer-Verzeichnis
$ mkdir tests
$ touch tests/__init__.py
$ touch tests/test_geometry.py
Schritt 2: Einen einfachen Test schreiben
# tests/test_geometry.py
import numpy as np
from numpy.typing import NDArray
import pytest
from road_profile_viewer.geometry import find_intersection
def test_find_intersection_finds_intersection_for_normal_angle() -> None:
"""
Test that find_intersection() returns a valid intersection
for a normal downward angle with a simple road profile.
Equivalence class: Normal downward angles (-90° < angle < 0°)
"""
# Arrange: Einfache aufwärts gehende Straße erstellen
x_road: NDArray[np.float64] = np.array([0, 10, 20, 30], dtype=np.float64)
y_road: NDArray[np.float64] = np.array([0, 2, 4, 6], dtype=np.float64)
angle: float = -10.0 # Abwärtswinkel
camera_x: float = 0.0
camera_y: float = 10.0 # Kamera über Straße
# Act: Schnittpunkt finden
x, y, dist = find_intersection(
x_road, y_road, angle, camera_x, camera_y
)
# Assert: Sollte einen Schnittpunkt finden
assert x is not None, "x coordinate should not be None"
assert y is not None, "y coordinate should not be None"
assert dist is not None, "distance should not be None"
assert dist > 0, "distance should be positive"
assert 0 <= x <= 30, "intersection x should be within road bounds"
Schritt 3: Den Test ausführen
$ uv run pytest tests/test_geometry.py -v
============================= test session starts ==============================
collected 1 item
tests/test_geometry.py::test_find_intersection_finds_intersection_for_normal_angle PASSED [100%]
============================== 1 passed in 0.05s ===============================
✅ Ihr erster Unit-Test besteht!
5.6 Clean Code Principles für Testing
Bevor wir tiefer in Äquivalenzklassen eintauchen, lassen Sie uns einige Clean-Code-Prinzipien für das Schreiben wartbarer Tests etablieren. Diese Prinzipien stammen aus Robert C. Martins “Clean Code” und sind essentiell, um Ihre Test-Suite lesbar und wartbar zu halten.
5.6.1 Ein Konzept pro Test
Prinzip: Jeder Test sollte EIN Konzept oder Verhalten überprüfen, nicht mehrere unabhängige Dinge.
Warum? Wenn ein Test fehlschlägt, wollen Sie sofort wissen, was kaputt ist. Tests mit mehreren Konzepten machen das Debugging schwerer.
❌ Schlechtes Beispiel: Mehrere Konzepte testen
def test_find_intersection_everything():
"""Dieser Test versucht zu viele Dinge auf einmal zu überprüfen."""
# Test Abwärtswinkel
x_road = np.array([0, 10, 20, 30], dtype=np.float64)
y_road = np.array([0, 2, 4, 6], dtype=np.float64)
x, y, dist = find_intersection(x_road, y_road, -10.0, 0.0, 10.0)
assert x is not None # Konzept 1: Abwärtswinkel findet Schnittpunkt
# Test vertikaler Winkel
x2, y2, dist2 = find_intersection(x_road, y_road, 90.0)
assert x2 is None # Konzept 2: Vertikaler Winkel gibt None zurück
# Test leere Arrays
x3, y3, dist3 = find_intersection(np.array([]), np.array([]), -10.0)
assert x3 is None # Konzept 3: Leere Arrays geben None zurück
Problem: Wenn dieser Test fehlschlägt, welches Konzept ist kaputt? Sie müssen den gesamten Test lesen, um es herauszufinden.
✅ Gutes Beispiel: Ein Konzept pro Test
def test_find_intersection_returns_intersection_for_downward_angle():
"""Test dass Abwärtswinkel Schnittpunkte finden."""
x_road = np.array([0, 10, 20, 30], dtype=np.float64)
y_road = np.array([0, 2, 4, 6], dtype=np.float64)
x, y, dist = find_intersection(x_road, y_road, -10.0, 0.0, 10.0)
assert x is not None
def test_find_intersection_returns_none_for_vertical_angle():
"""Test dass vertikale Winkel None zurückgeben."""
x_road = np.array([0, 10, 20, 30], dtype=np.float64)
y_road = np.array([0, 2, 4, 6], dtype=np.float64)
x, y, dist = find_intersection(x_road, y_road, 90.0)
assert x is None
def test_find_intersection_returns_none_for_empty_arrays():
"""Test dass leere Arrays None zurückgeben."""
x, y, dist = find_intersection(np.array([]), np.array([]), -10.0)
assert x is None
Vorteile:
- ✅ Testname sagt Ihnen genau, was fehlgeschlagen ist
- ✅ Jeder Test ist fokussiert und einfach zu verstehen
- ✅ Einfach einzelne Konzepte auszuführen oder zu überspringen
- ✅ Klare Testausgabe bei Fehlern
5.6.2 Ein Assert pro Test (mit pragmatischen Ausnahmen)
Prinzip: Idealerweise sollte jeder Test EIN Assert-Statement haben. Aber es gibt vernünftige Ausnahmen.
Wann EIN Assert ideal ist:
def test_find_intersection_returns_positive_distance():
"""Test dass Distanz immer positiv ist, wenn Schnittpunkt gefunden."""
x_road = np.array([0, 10, 20], dtype=np.float64)
y_road = np.array([0, 2, 4], dtype=np.float64)
x, y, dist = find_intersection(x_road, y_road, -10.0, 0.0, 10.0)
assert dist > 0 # Ein Konzept: Distanz ist positiv
Wann MEHRERE Asserts akzeptabel sind:
Mehrere Asserts sind okay, wenn sie verschiedene Aspekte desselben Konzepts testen:
def test_find_intersection_returns_valid_coordinates():
"""Test dass zurückgegebene Koordinaten innerhalb erwarteter Grenzen liegen (gleiches Konzept)."""
x_road = np.array([0, 10, 20, 30], dtype=np.float64)
y_road = np.array([0, 2, 4, 6], dtype=np.float64)
x, y, dist = find_intersection(x_road, y_road, -10.0, 0.0, 10.0)
# Alle diese Asserts überprüfen das GLEICHE Konzept: gültige Koordinatengrenzen
assert x is not None, "x sollte nicht None sein"
assert y is not None, "y sollte nicht None sein"
assert 0 <= x <= 30, f"x sollte in [0, 30] sein, bekam {x}"
assert 0 <= y <= 6, f"y sollte in [0, 6] sein, bekam {y}"
Das ist akzeptabel, weil:
- Alle Asserts testen das gleiche Konzept: “zurückgegebene Koordinaten sind gültig”
- Sie sind verwandte Aspekte eines Verhaltens
- Fehlschlagen eines Asserts deutet auf das gleiche Problem hin: ungültige Koordinaten
Wann man in mehrere Tests AUFTEILEN sollte:
Wenn Asserts verschiedene Konzepte testen, teile sie auf:
# ❌ Schlecht: Zwei verschiedene Konzepte in einem Test
def test_find_intersection_coordinates_and_distance():
x_road = np.array([0, 10, 20], dtype=np.float64)
y_road = np.array([0, 2, 4], dtype=np.float64)
x, y, dist = find_intersection(x_road, y_road, -10.0, 0.0, 10.0)
assert x > 0 # Konzept 1: x ist positiv
assert dist > 0 # Konzept 2: Distanz ist positiv (anderes Konzept!)
# ✅ Gut: In zwei Tests aufgeteilt
def test_find_intersection_returns_positive_x():
x_road = np.array([0, 10, 20], dtype=np.float64)
y_road = np.array([0, 2, 4], dtype=np.float64)
x, y, dist = find_intersection(x_road, y_road, -10.0, 0.0, 10.0)
assert x > 0
def test_find_intersection_returns_positive_distance():
x_road = np.array([0, 10, 20], dtype=np.float64)
y_road = np.array([0, 2, 4], dtype=np.float64)
x, y, dist = find_intersection(x_road, y_road, -10.0, 0.0, 10.0)
assert dist > 0
5.6.3 Beschreibende Testnamen
Prinzip: Testnamen sollten beschreiben was getestet wird, welches Szenario, und was das erwartete Ergebnis ist.
Muster: test_[funktion]_[szenario]_[erwartetes_verhalten]()
❌ Schlechte Testnamen:
def test_1(): # Was testet das?
pass
def test_intersection(): # Zu vage
pass
def test_angle(): # Welcher Winkel? Was ist damit?
pass
def test_edge_case(): # Welcher Randfall?
pass
✅ Gute Testnamen:
def test_find_intersection_returns_none_for_empty_arrays():
"""Klar: Funktion + Szenario + erwartetes Ergebnis"""
pass
def test_find_intersection_finds_intersection_for_downward_angle():
"""Sie wissen genau was das testet"""
pass
def test_find_intersection_returns_none_when_ray_misses_road():
"""Szenario und Ergebnis sind explizit"""
pass
def test_find_intersection_handles_vertical_angle_gracefully():
"""Klare Randfall-Behandlung"""
pass
Warum das wichtig ist:
Testausgabe bei Fehler:
❌ FAILED test_1
# Was sagt Ihnen das? Nichts!
✅ FAILED test_find_intersection_returns_none_for_empty_arrays
# Sofort klar: Behandlung leerer Arrays ist kaputt
Real-World-Beispiel von find_intersection:
class TestFindIntersection:
"""Test-Suite für find_intersection() Funktion."""
def test_find_intersection_returns_intersection_for_downward_angle(self):
"""Test normaler Abwärtswinkel findet Schnittpunkt."""
pass
def test_find_intersection_returns_none_for_vertical_angle(self):
"""Test vertikaler Winkel (90°) gibt None zurück."""
pass
def test_find_intersection_returns_none_for_empty_road_arrays(self):
"""Test leere Arrays geben None elegant zurück."""
pass
def test_find_intersection_returns_none_for_single_point_road(self):
"""Test Ein-Punkt-Straße (keine Segmente) gibt None zurück."""
pass
def test_find_intersection_finds_intersection_on_first_segment(self):
"""Test Schnittpunkterkennung auf erstem Straßensegment."""
pass
Wenn Sie diese Tests ausführen:
$ uv run pytest tests/test_geometry.py -v
tests/test_geometry.py::TestFindIntersection::test_find_intersection_returns_intersection_for_downward_angle PASSED
tests/test_geometry.py::TestFindIntersection::test_find_intersection_returns_none_for_vertical_angle PASSED
tests/test_geometry.py::TestFindIntersection::test_find_intersection_returns_none_for_empty_road_arrays FAILED
Sie wissen sofort: “Leere-Array-Behandlung ist kaputt” ohne irgendeinen Code zu lesen!
5.6.4 Zusammenfassung der Clean Code Principles für Testing
| Prinzip | Richtlinie | Warum es wichtig ist |
|---|---|---|
| Ein Konzept pro Test | Jeder Test überprüft EIN Verhalten/Konzept | Wenn Test fehlschlägt, wissen Sie sofort was kaputt ist |
| Ein Assert pro Test | Idealerweise ein Assert; mehrere okay wenn gleiches Konzept getestet wird | Fehler sind eindeutig; klar was schief ging |
| Beschreibende Namen | test_[funktion]_[szenario]_[erwartet]() |
Testausgabe ist selbstdokumentierend; kein Code lesen nötig |
| AAA-Muster | Arrange-Act-Assert (aus Abschnitt 5.2) | Tests sind lesbar und konsistent |
Diese Prinzipien arbeiten zusammen:
# ✅ Perfekter Test: Ein Konzept, ein Assert, beschreibender Name, AAA-Muster
def test_find_intersection_returns_positive_distance_for_valid_intersection():
"""Test dass Distanz von Kamera zu Schnittpunkt immer positiv ist."""
# Arrange
x_road = np.array([0, 10, 20, 30], dtype=np.float64)
y_road = np.array([0, 2, 4, 6], dtype=np.float64)
# Act
x, y, dist = find_intersection(x_road, y_road, -10.0, 0.0, 10.0)
# Assert
assert dist > 0, f"Erwartete positive Distanz, bekam {dist}"
Vorteile dieser Prinzipien:
- ✅ Wartbarkeit: Einfach Tests zu aktualisieren, wenn Code sich ändert
- ✅ Debugging: Fehler zeigen sofort auf das Problem
- ✅ Dokumentation: Testnamen erklären, was der Code tun sollte
- ✅ Vertrauen: Klare Tests geben Ihnen Vertrauen zum Refactoring
6. Wie geht es weiter?
Wir haben die Grundlagen behandelt:
- ✅ Die Testing-Pyramide und warum Unit-Tests die Basis bilden
- ✅ Das AAA-Muster zur Strukturierung von Tests
- ✅ pytest Framework Grundlagen
- ✅ Clean Code Prinzipien für Tests
Aber es gibt eine kritische Frage, die wir noch nicht beantwortet haben: Wie entscheiden Sie, WELCHE Testfälle Sie schreiben sollen?
Sie können nicht alles erschöpfend testen. Wie wählen Sie also klug aus?
Weiter zu Kapitel 03 (Äquivalenzklassen): Äquivalenzklassen um fortgeschrittene Testdesign-Techniken zu lernen.