Empfohlenes Verhältnis:
Warum? Geschwindigkeit ist wichtig für die Feedback-Schleife:
Wenn Sie eine Zeile in find_intersection() ändern:
Schnelle Tests:
Oft laufen lassen → Bugs sofort finden → Schnell beheben → Im Flow bleiben
Langsame Tests:
Selten laufen lassen → Bugs häufen sich → Schwer zu debuggen → Kontextwechsel
Beispielvergleich:
Das ist verkehrt herum! Gleiche Farben, umgedrehte Form — jede Ebene wird zur Belastung.
1. Langsames Feedback:
2. Schwer zu debuggen:
3. Zerbrechlich:
4. Teufelskreis:
Warum glauben Sie, dass Entwickler Testkegel erstellen?
Häufige Gründe:
Heutiges Ziel: Unit-Testing so einfach machen, dass Sie es bevorzugen!
Jetzt, da wir wissen, WARUM Unit-Tests wichtig sind, lernen wir, WIE man sie schreibt.
Schlüsselthemen:
Definition:
Ein Unit-Test überprüft, dass eine einzelne Einheit (Funktion, Methode, Klasse) isoliert korrekt funktioniert.
Schlüsselmerkmale:
Beispiel: Testen Sie find_intersection() ohne Dash-App zu starten
def test_find_intersection_normal_angle():
"""Test that find_intersection() works with a normal downward angle."""
# Arrange: Set up test data
x_road = np.array([0, 10, 20, 30])
y_road = np.array([0, 2, 3, 4])
angle = -10.0
# Act: Call the function
x, y, dist = find_intersection(x_road, y_road, angle)
# Assert: Verify the result
assert x is not None, "Should find an intersection"
assert dist > 0, "Distance should be positive"
Das ist es! Einfach, fokussiert, schnell.
Jeder gute Unit-Test folgt dieser Struktur:
def test_example():
# ARRANGE: Testdaten und Bedingungen einrichten
input_data = prepare_test_data()
expected_result = calculate_expected()
# ACT: Zu testende Funktion aufrufen
actual_result = function_under_test(input_data)
# ASSERT: Überprüfen, ob Ergebnis den Erwartungen entspricht
assert actual_result == expected_result
Warum dieses Pattern? Klare Struktur macht Tests lesbar, leicht zu sehen, was getestet wird, leicht zu debuggen, wenn Tests fehlschlagen.
def test_find_intersection_finds_intersection_for_normal_angle():
# ARRANGE: Einfache Straße nach oben erstellen
x_road = np.array([0, 10, 20, 30], dtype=np.float64)
y_road = np.array([0, 2, 4, 6], dtype=np.float64)
angle = -10.0
camera_x = 0.0
camera_y = 10.0 # Kamera über der 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
assert dist > 0
assert 0 <= x <= 30 # Innerhalb der Straßengrenzen
Frage: Können wir nicht einfach Tests als normale Python-Funktionen schreiben?
# Ohne Test-Framework:
def manual_test():
x, y, dist = find_intersection(x_road, y_road, -10.0)
if x is None:
print("
FAILED: x should not be None")
return False
if dist <= 0:
print("
FAILED: distance should be positive")
return False
print("
PASSED")
return True
# Test manuell ausführen
manual_test()
Manuelles Testen hat viele Probleme:
Enter pytest: Der Industriestandard für Test-Frameworks
# Automatische Test-Erkennung - findet alle test_*.py Dateien
$ uv run pytest
# Mit detaillierter Ausgabe
$ uv run pytest -v
# Spezifische Test-Datei ausführen
$ uv run pytest tests/test_geometry.py
# Tests ausführen, die einem Muster entsprechen
$ uv run pytest -k "intersection"
# Bei erstem Fehler stoppen
$ uv run pytest -x
Was macht pytest zum Industriestandard:
assert (keine speziellen Methoden)test_*.py Dateien$ uv run pytest tests/test_geometry.py -v
tests/test_geometry.py::test_normal_angle PASSED [ 20%]
tests/test_geometry.py::test_empty_array FAILED [ 60%]
def test_empty_array():
> x, y, dist = find_intersection(x_road, y_road, -10.0)
E IndexError: index 0 is out of bounds
1 failed, 4 passed in 0.12s
Zeigt genau, welcher Test fehlgeschlagen ist, den Fehler und Pass/Fail-Anzahl
Lassen Sie uns gemeinsam einen Test für find_intersection() schreiben
Ziel: Den normalen Abwärtswinkel-Fall testen
Schritt 1: Test-Dateistruktur erstellen
$ mkdir tests
$ touch tests/__init__.py
$ touch tests/test_geometry.py
Schritt 2: Den Test schreiben
Schritt 3: Ihn ausführen und sehen, dass er funktioniert!
# tests/test_geometry.py
import numpy as np
from road_profile_viewer.geometry import find_intersection
def test_find_intersection_finds_intersection_for_normal_angle():
# 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 x is not None
assert dist > 0
$ 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 funktioniert!
Herzlichen Glückwunsch! Sie haben Ihren ersten pytest Unit-Test geschrieben.
Interaktive Übung: Lassen Sie uns einen Test für angle = 90° (vertikaler Winkel) schreiben
Was sollte passieren?
None, None, None zurück (kann keinen Schnittpunkt für vertikalen Strahl finden)Sie sind dran: Wie würde der Test aussehen?
Hinweis: Verwenden Sie das AAA-Pattern!
Frage: Warum können wir nicht einfach jede mögliche Eingabe testen?
Lassen Sie uns die Mathematik für find_intersection() durchrechnen:
def find_intersection(x_road, y_road, angle_degrees,
camera_x=0, camera_y=1.5):
"""Find intersection between camera ray and road profile."""
# ...
Parameter:
angle_degrees: float (-180° bis 180°)camera_x: floatcamera_y: floatx_road, y_road: Arrays (variable Länge und Werte)Nur angle_degrees (ein Float):
Gesamtzeit, um nur angle_degrees zu testen:
1.000.000 Tests × 0,001 Sekunden = 1.000 Sekunden ≈ 17 Minuten
Nicht zu schlecht! Aber wir haben 3 Float-Parameter...
Alle Kombinationen von 3 Float-Parametern testen:
1.000.000³ = 1 Trillion Tests
Bei 1ms pro Test = 32 MILLIARDEN JAHRE
Alter des Universums: ~14 Milliarden Jahre
Fazit: Erschöpfendes Testen ist buchstäblich unmöglich
Da erschöpfendes Testen unmöglich ist, brauchen wir intelligente Teststrategien:
Heute: Wir lernen die Grundlagen (AAA-Pattern, pytest, Clean Code)
Nächstes Mal: Wir lernen intelligente Teststrategien (Äquivalenzklassen, Grenzen)
Drei Schlüsselprinzipien aus Robert C. Martins "Clean Code":
test_[funktion]_[szenario]_[erwartet]()Warum? Wenn ein Test fehlschlägt, wissen Sie sofort, was kaputt ist.
Schlecht: Mehrere Konzepte testen
def test_find_intersection_everything():
x, y, dist = find_intersection(x_road, y_road, -10.0, 0.0, 10.0)
assert x is not None # Konzept 1: Abwärtswinkel
x2, y2, dist2 = find_intersection(x_road, y_road, 90.0)
assert x2 is None # Konzept 2: Vertikaler Winkel
x3, y3, dist3 = find_intersection(np.array([]), np.array([]), -10.0)
assert x3 is None # Konzept 3: Leere Arrays
Problem: Welches Konzept ist fehlgeschlagen?
Gut: Drei separate Tests
def test_find_intersection_
returns_intersection_
for_downward_angle():
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():
x, y, dist = find_intersection(
x_road, y_road, 90.0)
assert x is None
Dritter Test:
def test_find_intersection_
returns_none_for_empty_arrays():
x, y, dist = find_intersection(
np.array([]),
np.array([]),
-10.0)
assert x is None
Idealerweise sollte jeder Test EINE Assert-Anweisung haben.
Aber es gibt vernünftige Ausnahmen!
Wann EIN Assert ideal ist:
def test_find_intersection_returns_positive_distance():
"""Test that distance is always positive when intersection found."""
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
Mehrere Asserts OK, wenn GLEICHES Konzept getestet wird:
def test_find_intersection_returns_valid_coordinates():
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 überprüfen GLEICHES Konzept: gültige Koordinatengrenzen
assert x is not None
assert y is not None
assert 0 <= x <= 30
assert 0 <= y <= 6
Akzeptabel: Alle Asserts testen Koordinatenvalidität (ein Konzept)
Schlecht: Verschiedene Konzepte
def test_find_intersection_
coordinates_and_distance():
x, y, dist = find_intersection(
x_road, y_road,
-10.0, 0.0, 10.0)
assert x > 0 # Konzept 1
assert dist > 0 # Konzept 2
Problem: Zwei verschiedene Konzepte in einem Test
Gut: Separate Tests
def test_find_intersection_
returns_positive_x():
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, y, dist = find_intersection(
x_road, y_road,
-10.0, 0.0, 10.0)
assert dist > 0
Testnamen sollten beschreiben:
Pattern: test_[funktion]_[szenario]_[erwartetes_verhalten]()
Schlechte Testnamen:
def test_1(): # Was testet das?
def test_intersection(): # Zu vage
def test_angle(): # Welcher Winkel? Was ist damit?
def test_edge_case(): # Welcher Randfall?
Gute Testnamen:
def test_find_intersection_returns_none_for_empty_arrays():
"""Klar: Funktion + Szenario + erwartetes Ergebnis"""
def test_find_intersection_finds_intersection_for_downward_angle():
"""Sie wissen genau, was das testet"""
def test_find_intersection_returns_none_when_ray_misses_road():
"""Szenario und Ergebnis sind explizit"""
def test_find_intersection_handles_vertical_angle_gracefully():
"""Klare Randfall-Behandlung"""
Test-Ausgabe, wenn ein Fehler auftritt:
FAILED test_1
# Was sagt Ihnen das? Nichts!
FAILED test_find_intersection_returns_none_for_empty_arrays
# Sofort klar: Behandlung leerer Arrays ist kaputt
Sie wissen sofort, was kaputt ist ohne Code zu lesen!
Wie würden Sie einen Test für dieses Szenario benennen?
Testen Sie, dass find_intersection() None zurückgibt, wenn die Kamera unter der Straße ist und der Strahl nach unten zeigt.
Denken Sie darüber nach:
find_intersectionWie lautet Ihr Testname?
| Prinzip | Richtlinie | Warum es wichtig ist |
|---|---|---|
| Ein Konzept pro Test | Jeder Test überprüft EIN Verhalten | Wenn Test fehlschlägt, wissen Sie sofort, was kaputt ist |
| Ein Assert pro Test | Idealerweise eins; mehrere OK wenn gleiches Konzept | Fehler sind eindeutig |
| Beschreibende Namen | test_[funktion]_[szenario]_[erwartet]() |
Test-Ausgabe ist selbstdokumentierend |
| AAA-Pattern | Arrange-Act-Assert | Tests sind lesbar und konsistent |
Diese Prinzipien machen Ihre Tests einfach zu lesen, debuggen und warten.
Wichtige Erkenntnisse:
Kapitel 03 (Grenzwertanalyse) wird behandeln:
Kapitel 03 (TDD und CI) wird behandeln:
Aber zuerst: Üben Sie, was Sie heute gelernt haben!