03 Übung: Test-Qualität Vergleich
November 2025 (5587 Words, 32 Minutes)
Einführung
Willkommen zu Testing Grundlagen: Test-Qualität Vergleich! Diese Übungen helfen dir, den Unterschied zwischen gut geschriebenen und schlecht geschriebenen Tests zu erkennen.
Lernziele:
- Identifizieren, was einen Test gut geschrieben vs. schlecht geschrieben macht
- Verletzungen von Clean Code Prinzipien für Testing erkennen
- Das AAA (Arrange-Act-Assert) Pattern verstehen
- Das “Ein Konzept pro Test” Prinzip anwenden
- Beschreibende Testnamen schreiben, die Intent kommunizieren
- Zwischen angemessenem und unangemessenem Gebrauch mehrerer Asserts unterscheiden
Anleitung:
- Studiere sowohl die “Schlechte Qualität” als auch “Gute Qualität” Versionen
- Identifiziere ALLE Unterschiede zwischen den beiden Versionen
- Verstehe, welche Clean Code Prinzipien angewendet werden
- Überlege, wie jede Verbesserung Tests wartbarer macht
- Klicke “Analyse anzeigen”, um deine Beobachtungen zu vergleichen
Schwierigkeit: Fortgeschritten Zeit: 10-15 Minuten pro Übung Voraussetzungen: Verständnis der Konzepte aus Kapitel 03 (Testing-Grundlagen)
Warum diese Übung wichtig ist: Gut geschriebene Tests sind dein Sicherheitsnetz. Wenn du Code refactorierst, geben dir gute Tests Vertrauen. Wenn Tests fehlschlagen, sagen dir gute Tests genau, was kaputt ist. Schlechte Tests verschwenden Zeit und bieten falsche Sicherheit.
Übung 1: Testnamen
Schlechte Qualität
import numpy as np
from road_profile_viewer.geometry import find_intersection
def test_1():
x_road = np.array([0, 10, 20, 30])
y_road = np.array([0, 2, 4, 6])
x, y, dist = find_intersection(x_road, y_road, -10.0)
assert x is not None
def test_2():
x_road = np.array([0, 10, 20, 30])
y_road = np.array([0, 2, 4, 6])
x, y, dist = find_intersection(x_road, y_road, 90.0)
assert x is None
Gute Qualität
import numpy as np
from road_profile_viewer.geometry import find_intersection
def test_find_intersection_finds_intersection_for_downward_angle():
"""Test dass abwärts gerichtete Winkel erfolgreich Straßenschnittpunkte finden."""
x_road = np.array([0, 10, 20, 30])
y_road = np.array([0, 2, 4, 6])
x, y, dist = find_intersection(x_road, y_road, -10.0)
assert x is not None
def test_find_intersection_returns_none_for_vertical_angle():
"""Test dass vertikale Winkel (90°) None zurückgeben."""
x_road = np.array([0, 10, 20, 30])
y_road = np.array([0, 2, 4, 6])
x, y, dist = find_intersection(x_road, y_road, 90.0)
assert x is None
Fragen
- Was sind die Hauptunterschiede zwischen Schlechter und Guter Qualität?
- Was passiert, wenn Tests fehlschlagen in jeder Version?
- Welche Prinzipien aus Clean Code für Testing werden angewendet?
💡 Analyse anzeigen
Hauptunterschiede
Testnamen:
- Schlecht:
test_1(),test_2()- bedeutungslos - Gut:
test_find_intersection_finds_intersection_for_downward_angle()- beschreibend- Folgt dem Muster:
test_[funktion]_[szenario]_[erwartetes_verhalten]()
- Folgt dem Muster:
Docstrings:
- Schlecht: Keine Docstrings
- Gut: Klare Docstrings, die erklären, was getestet wird
Auswirkung von Fehlern
Schlechte Qualität - Unhilfreiche Ausgabe:
FAILED test_1
# Fragen: Was ist kaputt? Welche Funktion? Welches Szenario?
# Du musst den Testcode lesen, um zu verstehen, was fehlgeschlagen ist
Gute Qualität - Sofort klar:
FAILED test_find_intersection_returns_none_for_vertical_angle
# Sofort klar: "Vertikale Winkelbehandlung ist kaputt"
# Keine Notwendigkeit, Code zu lesen - Testname sagt dir, was kaputt ist
Angewendete Prinzipien
- Beschreibende Testnamen
- Muster:
test_[funktion]_[szenario]_[erwartet]() - Kommuniziert: Was + Wie + Erwartetes Ergebnis
- Muster:
- Selbstdokumentierende Tests
- Testname + Docstring machen den Zweck des Tests kristallklar
- Zukünftige Entwickler (oder du in 6 Monaten) verstehen sofort
- Bessere Debugging-Erfahrung
- Wenn Test fehlschlägt, weißt du genau, was zu untersuchen ist
- Keine Zeit verschwendet, Testcode zu lesen, um zu verstehen, was er testet
Reale Auswirkung
Stelle dir ein Projekt mit 500 Tests vor. Wenn test_1() fehlschlägt:
- ❌ Du verschwendest 5-10 Minuten, den Test zu finden und zu lesen
- ❌ Multipliziere mit Dutzenden von Fehlern während des Refactorings
- ❌ Ergebnis: Stunden verschwendet
Wenn test_find_intersection_returns_none_for_vertical_angle() fehlschlägt:
- ✅ Du weißt sofort: “Vertikale Winkelbehandlung ist das Problem”
- ✅ Gehe direkt zu diesem Code
- ✅ Ergebnis: Fix in Minuten
Kernaussage: Beschreibende Namen sind nicht about Ausführlichkeit - sie sind about Zeitersparnis.
Übung 2: AAA Pattern
Schlechte Qualität
def test_distance_calculation():
x, y, dist = find_intersection(np.array([0, 10, 20, 30]), np.array([0, 2, 4, 6]), -10.0, 0.0, 10.0)
assert dist > 0 and x is not None
Gute Qualität
def test_find_intersection_returns_positive_distance():
"""Test dass Distanz positiv ist, wenn ein Schnittpunkt gefunden wird."""
# Arrange: Testdaten einrichten
x_road = np.array([0, 10, 20, 30])
y_road = np.array([0, 2, 4, 6])
angle = -10.0
camera_x = 0.0
camera_y = 10.0
# Act: Schnittpunkt finden
x, y, dist = find_intersection(x_road, y_road, angle, camera_x, camera_y)
# Assert: Verifizieren dass Distanz positiv ist
assert dist > 0
Fragen
- Welche strukturellen Unterschiede machen die Gute Version einfacher zu lesen?
- Warum ist die Trennung von Arrange-Act-Assert wichtig?
- Was macht die Assertion klarer in der Guten Version?
💡 Analyse anzeigen
Strukturelle Unterschiede
Schlechte Qualität:
- Alles in eine Zeile gequetscht
- Keine Variablennamen für Klarheit
- Mehrere Assertions kombiniert mit
and - Keine klare Struktur
Gute Qualität:
- Arrange Abschnitt: Testdaten klar definiert mit aussagekräftigen Variablennamen
- Act Abschnitt: Funktionsaufruf in eigener Zeile
- Assert Abschnitt: Einzelne, klare Assertion
- Kommentare markieren jeden Abschnitt
Warum AAA Struktur wichtig ist
1. Lesbarkeit:
# Schlecht: Was wird getestet?
x, y, dist = find_intersection(np.array([0, 10, 20, 30]), ...)
assert dist > 0 and x is not None
# Gut: Kristallklar was jeder Teil tut
# Arrange: Ich richte eine einfache aufwärts verlaufende Straße ein
# Act: Ich finde den Schnittpunkt
# Assert: Ich prüfe dass Distanz positiv ist
2. Debugging: Wenn Test fehlschlägt, weißt du sofort:
- Arrange fehlgeschlagen: Test-Setup ist falsch
- Act fehlgeschlagen: Funktion ist abgestürzt
- Assert fehlgeschlagen: Ergebnis entspricht nicht den Erwartungen
3. Wartung: Einfach, jeden Abschnitt unabhängig zu modifizieren:
# Brauche anderen Winkel zu testen? Ändere nur Arrange Abschnitt
# Arrange
angle = -45.0 # Geändert von -10.0
# Act und Assert Abschnitte unverändert
Assertions-Klarheit
Schlecht:
assert dist > 0 and x is not None
# Was ist fehlgeschlagen? Distanz oder x?
# Testet zwei Dinge (schlechte Praxis)
Gut:
assert dist > 0
# Eine Sache getestet: Distanz-Positivität
# Wenn es fehlschlägt, weißt du genau, was falsch ist
Kernaussage
Das AAA Pattern ist nicht about Regeln folgen - es ist about Tests in 6 Monaten lesbar machen, wenn du vergessen hast, was du getestet hast.
Übung 3: Ein Konzept pro Test
Schlechte Qualität
def test_find_intersection_everything():
"""Test find_intersection mit verschiedenen Eingaben."""
# Test normaler Winkel
x_road = np.array([0, 10, 20, 30])
y_road = np.array([0, 2, 4, 6])
x1, y1, dist1 = find_intersection(x_road, y_road, -10.0)
assert x1 is not None
# Test vertikaler Winkel
x2, y2, dist2 = find_intersection(x_road, y_road, 90.0)
assert x2 is None
# Test leere Arrays
x3, y3, dist3 = find_intersection(np.array([]), np.array([]), -10.0)
assert x3 is None
# Test horizontaler Winkel
x4, y4, dist4 = find_intersection(x_road, y_road, 0.0)
assert x4 is not None
Gute Qualität
def test_find_intersection_finds_intersection_for_normal_angle():
"""Test dass normale abwärts gerichtete Winkel Schnittpunkte finden."""
# Arrange
x_road = np.array([0, 10, 20, 30])
y_road = np.array([0, 2, 4, 6])
# Act
x, y, dist = find_intersection(x_road, y_road, -10.0)
# Assert
assert x is not None
def test_find_intersection_returns_none_for_vertical_angle():
"""Test dass vertikale Winkel None zurückgeben."""
# Arrange
x_road = np.array([0, 10, 20, 30])
y_road = np.array([0, 2, 4, 6])
# Act
x, y, dist = find_intersection(x_road, y_road, 90.0)
# Assert
assert x is None
def test_find_intersection_returns_none_for_empty_arrays():
"""Test dass leere Straßenarrays None zurückgeben."""
# Arrange
x_road = np.array([])
y_road = np.array([])
# Act
x, y, dist = find_intersection(x_road, y_road, -10.0)
# Assert
assert x is None
def test_find_intersection_finds_intersection_for_horizontal_angle():
"""Test dass horizontale Winkel (0°) Schnittpunkte finden."""
# Arrange
x_road = np.array([0, 10, 20, 30])
y_road = np.array([0, 2, 4, 6])
# Act
x, y, dist = find_intersection(x_road, y_road, 0.0)
# Assert
assert x is not None
Fragen
- Was passiert, wenn
test_find_intersection_everything()fehlschlägt? - Was sind die Vorteile von aufgeteilten Tests?
- Wie beeinflusst das Debugging und Wartung?
💡 Analyse anzeigen
Problem mit Multi-Konzept Tests
Wenn test_find_intersection_everything() fehlschlägt:
FAILED test_find_intersection_everything
# Welches Szenario ist fehlgeschlagen?
# - Normaler Winkel?
# - Vertikaler Winkel?
# - Leere Arrays?
# - Horizontaler Winkel?
# Du musst den ganzen Test lesen, um es herauszufinden!
Schlimmer: Wenn die erste Assertion fehlschlägt, stoppt pytest dort. Du weißt nicht einmal, ob die anderen Szenarien funktionieren!
# Wenn das fehlschlägt:
assert x1 is not None # ❌ Schlägt hier fehl
# Du testest diese nie:
assert x2 is None # ❓ Unbekannt
assert x3 is None # ❓ Unbekannt
assert x4 is not None # ❓ Unbekannt
Vorteile von Ein Konzept pro Test
1. Klare Fehlermeldungen:
# Schlechte Qualität - vage
FAILED test_find_intersection_everything
# Gute Qualität - spezifisch
FAILED test_find_intersection_returns_none_for_vertical_angle
FAILED test_find_intersection_returns_none_for_empty_arrays
# Jetzt weißt du SOWOHL vertikale Winkel ALS AUCH leere Arrays sind kaputt!
2. Unabhängiges Testen: Jedes Szenario wird unabhängig getestet:
- Vertikaler Winkel Test schlägt fehl → Verhindert nicht, dass Empty Array Test läuft
- Sehe alle Fehler auf einmal, nicht einen nach dem anderen
- Schnelleres Debugging (fixe alle Probleme auf einmal)
3. Selektives Testen:
# Führe nur ein Szenario aus
$ pytest -k "vertical_angle"
# Führe alle außer einem Szenario aus
$ pytest -k "not empty_arrays"
4. Bessere Git History:
# Schlecht - unklar was sich geändert hat
commit: "fix test_find_intersection_everything"
# Gut - klar was hinzugefügt/gefixt wurde
commit: "add test for vertical angle handling"
commit: "fix empty array test"
Reales Szenario
Du refactorierst find_intersection(), um leere Arrays besser zu behandeln.
Mit Schlechter Qualität:
- Führe
test_find_intersection_everything()aus - Es schlägt fehl (aber du weißt nicht warum, bis du den ganzen Test liest)
- Debugge und fixe
- Führe gesamten Test erneut aus
- Wiederhole
Mit Guter Qualität:
- Führe
pytest -k "empty_arrays"aus - Test schlägt fehl mit klarer Meldung: “empty arrays test failed”
- Debugge und fixe
- Führe nur diesen Test erneut aus (schnell!)
- Alle anderen Tests laufen noch (Vertrauen!)
Kernaussage
“Teste eine Sache auf einmal” ist nicht about pedantisch sein - es ist about:
- Klare, spezifische Fehlermeldungen
- Unabhängige Testausführung
- Schnelleres Debugging
- Bessere Testorganisation
Übung 4: Mehrere Asserts - Angemessen vs Unangemessen
Beispiel A: Unangemessener Gebrauch
def test_find_intersection_mixed_concerns():
"""Test verschiedene Eigenschaften von find_intersection."""
# Arrange
x_road = np.array([0, 10, 20, 30])
y_road = np.array([0, 2, 4, 6])
# Act
x, y, dist = find_intersection(x_road, y_road, -10.0, 0.0, 10.0)
# Assert - testet VERSCHIEDENE Konzepte
assert x > 0 # Konzept 1: x ist positiv
assert dist > 0 # Konzept 2: Distanz ist positiv
assert dist < 100 # Konzept 3: Distanz ist vernünftig
Beispiel B: Angemessener Gebrauch
def test_find_intersection_returns_positive_x():
"""Test dass x-Koordinate positiv ist für abwärts gerichtete Winkel."""
# Arrange
x_road = np.array([0, 10, 20, 30])
y_road = np.array([0, 2, 4, 6])
# Act
x, y, dist = find_intersection(x_road, y_road, -10.0, 0.0, 10.0)
# Assert
assert x > 0
def test_find_intersection_returns_valid_distance():
"""Test dass Distanz positiv und vernünftig ist."""
# Arrange
x_road = np.array([0, 10, 20, 30])
y_road = np.array([0, 2, 4, 6])
# Act
x, y, dist = find_intersection(x_road, y_road, -10.0, 0.0, 10.0)
# Assert - testet dasselbe Konzept (gültige Distanz)
assert dist > 0, "Distanz sollte positiv sein"
assert dist < 100, "Distanz sollte vernünftig sein"
Fragen
- Warum ist Beispiel A unangemessen?
- Warum ist Beispiel B’s Gebrauch mehrerer Asserts akzeptabel?
- Wie entscheidest du, wann Tests aufgeteilt werden sollen?
💡 Analyse anzeigen
Beispiel A: Das Problem
Testet DREI verschiedene Konzepte in einem Test:
assert x > 0 # Konzept 1: "x-Koordinate ist positiv"
assert dist > 0 # Konzept 2: "Distanz ist positiv"
assert dist < 100 # Konzept 3: "Distanz ist vernünftig"
Warum das problematisch ist:
1. Unklarer Fehler:
FAILED test_find_intersection_mixed_concerns
# Welches Konzept ist fehlgeschlagen?
# - Ist x falsch?
# - Ist Distanz negativ?
# - Ist Distanz unvernünftig groß?
# Du musst untersuchen!
2. Gekoppelte Konzepte:
- x-Koordinate und Distanz sind unabhängige Eigenschaften
- Sie zusammen zu testen erzeugt unnötige Kopplung
- Wenn sich Distanzberechnung ändert, könnte dieser Test fehlschlagen, auch wenn x noch korrekt ist
3. Schwer zu warten:
- Willst du Distanzvalidierungslogik ändern? Musst diesen Test modifizieren
- Willst du mehr x-Koordinaten Checks hinzufügen? Musst diesen Test modifizieren
- Änderungen an einem Konzept beeinflussen das andere
Beispiel B: Angemessene Mehrere Asserts
Erster Test - EIN Konzept (x-Positivität):
def test_find_intersection_returns_positive_x():
assert x > 0 # Einzelnes Konzept: x ist positiv
Zweiter Test - EIN Konzept (Distanzgültigkeit):
def test_find_intersection_returns_valid_distance():
assert dist > 0 # Beide Asserts testen dasselbe Konzept:
assert dist < 100 # "Distanz ist gültig"
Warum mehrere Asserts hier OK sind:
- Beide Asserts verifizieren das gleiche Konzept: “Distanz ist gültig”
- Sie sind verschiedene Aspekte derselben Sache (positiv UND vernünftig)
- Fehlschlagen von beiden Asserts zeigt das gleiche Problem an: Distanzvalidierung ist kaputt
Entscheidungsframework
Frage dich selbst: “Welches Konzept verifiziert dieser Test?”
Kann in einem Satz beschreiben? → OK für mehrere Asserts
# "Dieser Test verifiziert dass Koordinaten innerhalb der Straßengrenzen sind"
def test_coordinates_within_bounds():
assert 0 <= x <= 30 # ✅ Gleiches Konzept
assert 0 <= y <= 6 # ✅ Gleiches Konzept
Brauche mehrere Sätze? → Tests aufteilen
# "Dieser Test verifiziert x ist positiv UND Distanz ist positiv UND..."
# ❌ Das sind mehrere Konzepte - aufteilen!
Praktisches Beispiel
Unangemessen - Gemischte Concerns:
def test_road_profile_generation():
x, y = generate_road_profile()
assert len(x) == 100 # Konzept 1: Array-Länge
assert np.min(x) >= 0 # Konzept 2: x-Grenzen
assert np.std(y) > 0.1 # Konzept 3: y-Variation
assert np.max(y) < 10 # Konzept 4: y-Grenzen
Wenn das fehlschlägt, was ist kaputt? Länge? Grenzen? Variation? Keine Ahnung!
Angemessen - Fokussierte Konzepte:
def test_road_profile_has_correct_length():
x, y = generate_road_profile()
assert len(x) == 100
def test_road_profile_x_within_bounds():
x, y = generate_road_profile()
assert np.min(x) >= 0 # Gleiches Konzept: x-Grenzen
assert np.max(x) <= 100 # Gleiches Konzept: x-Grenzen
def test_road_profile_y_has_variation():
x, y = generate_road_profile()
assert np.std(y) > 0.1
def test_road_profile_y_within_bounds():
x, y = generate_road_profile()
assert np.min(y) >= -5 # Gleiches Konzept: y-Grenzen
assert np.max(y) <= 10 # Gleiches Konzept: y-Grenzen
Jetzt wenn ein Test fehlschlägt: Testname sagt dir genau, welche Eigenschaft kaputt ist!
Kernaussage
Mehrere Asserts sind OK wenn:
- ✅ Testen verschiedene Aspekte desselben Konzepts
- ✅ Alle Asserts schlagen aus demselben zugrunde liegenden Grund fehl
In separate Tests aufteilen wenn:
- ❌ Testen verschiedene Konzepte
- ❌ Asserts könnten aus unabhängigen Gründen fehlschlagen
Daumenregel: Wenn du den Test nicht mit einem Konzept benennen kannst, testest du zu viele Dinge.
Übung 5: Test-Isolation
Schlechte Qualität (Abhängige Tests)
# Globaler Zustand geteilt zwischen Tests
test_road_data = None
def test_generate_road_profile():
"""Test Straßenprofil-Generierung."""
global test_road_data
test_road_data = generate_road_profile(num_points=100)
assert test_road_data is not None
def test_road_profile_has_correct_length():
"""Test dass generierte Straße korrekte Länge hat."""
global test_road_data
# Hängt davon ab dass test_generate_road_profile zuerst läuft!
assert len(test_road_data[0]) == 100
def test_road_profile_x_values():
"""Test dass x-Werte korrekt sind."""
global test_road_data
# Hängt auch von test_generate_road_profile ab!
assert np.max(test_road_data[0]) <= 80
Gute Qualität (Isolierte Tests)
def test_generate_road_profile_returns_non_null():
"""Test dass Straßenprofil-Generierung Daten zurückgibt."""
# Arrange
num_points = 100
# Act
road_data = generate_road_profile(num_points=num_points)
# Assert
assert road_data is not None
def test_generate_road_profile_has_correct_length():
"""Test dass generierte Straße korrekte Anzahl Punkte hat."""
# Arrange
num_points = 100
# Act
x_road, y_road = generate_road_profile(num_points=num_points)
# Assert
assert len(x_road) == 100
assert len(y_road) == 100
def test_generate_road_profile_x_values_within_bounds():
"""Test dass x-Werte innerhalb erwarteter Bereich sind."""
# Arrange
num_points = 100
x_max = 80
# Act
x_road, y_road = generate_road_profile(num_points=num_points, x_max=x_max)
# Assert
assert np.min(x_road) >= 0
assert np.max(x_road) <= x_max
Fragen
- Was passiert, wenn Tests in unterschiedlicher Reihenfolge laufen?
- Was sind die Probleme mit globalem Zustand?
- Wie verbessern isolierte Tests die Zuverlässigkeit?
💡 Analyse anzeigen
Probleme mit Abhängigen Tests
1. Reihenfolge-Abhängigkeit:
# Schlechte Qualität - MUSS in dieser Reihenfolge laufen:
test_generate_road_profile() # Setzt globalen Zustand
test_road_profile_has_correct_length() # Liest globalen Zustand
test_road_profile_x_values() # Liest globalen Zustand
# Was wenn pytest sie anders ausführt?
$ pytest -k "x_values" # ❌ Schlägt fehl! Globaler Zustand nicht gesetzt
$ pytest tests/ --reverse # ❌ Schlägt fehl! Falsche Reihenfolge
2. Debugging-Alptraum:
# Führe einen Test isoliert aus
$ pytest tests/test_road.py::test_road_profile_x_values
# ❌ FAILED: 'NoneType' object has no attribute '__getitem__'
# Warum? Weil test_generate_road_profile nicht zuerst gelaufen ist!
# Testfehler hat nichts mit der eigentlichen Logik zu tun
3. Instabile Tests:
# Manchmal besteht (wenn Reihenfolge richtig ist)
$ pytest tests/ # ✅ PASSED (Glück!)
# Manchmal schlägt fehl (wenn Reihenfolge falsch ist)
$ pytest tests/ --random-order # ❌ FAILED (Pech!)
# Entwickler verschwenden Stunden mit Untersuchung von "manchmal schlägt es fehl"
4. Kann nicht parallel laufen:
# Parallele Ausführung schlägt fehl
$ pytest -n 4 # ❌ Alle Tests schlagen fehl! Globaler Zustand Kollision
Vorteile isolierter Tests
1. Laufen Überall, Jederzeit:
# ✅ Führe einen Test aus
$ pytest tests/test_road.py::test_generate_road_profile_x_values_within_bounds
# Funktioniert perfekt!
# ✅ Führe in zufälliger Reihenfolge aus
$ pytest tests/ --random-order
# Alle bestanden!
# ✅ Führe parallel aus
$ pytest -n 8
# Schnell UND zuverlässig!
2. Klare Testfehler:
# Wenn Test fehlschlägt, weißt du es ist die eigentliche Logik
def test_generate_road_profile_x_values_within_bounds():
x_road, y_road = generate_road_profile(num_points=100, x_max=80)
assert np.max(x_road) <= 80 # Wenn das fehlschlägt, ist x-Generierung kaputt
# Nicht wegen:
# - Fehlendem Setup
# - Falscher Testreihenfolge
# - Globalem Zustand Korruption
3. Unabhängiges Debugging:
Jeder Test ist in sich geschlossen:
def test_generate_road_profile_x_values_within_bounds():
# Alles Benötigte ist genau hier
x_road, y_road = generate_road_profile(num_points=100, x_max=80)
assert np.max(x_road) <= 80
# Keine Notwendigkeit zu:
# - Setup-Funktion finden
# - Globalen Zustand prüfen
# - Testreihenfolge verstehen
4. Wartung:
# Willst du einen Test löschen? Lösche ihn einfach!
# Keine Sorge about:
# - "Hängt ein anderer Test davon ab?"
# - "Wird das andere Tests brechen?"
# Willst du einen Test hinzufügen? Füge ihn einfach hinzu!
# Keine Sorge about:
# - "In welcher Reihenfolge sollte das laufen?"
# - "Muss ich globalen Zustand einrichten?"
Wie Isolation erreichen
Verwende Fixtures (pytest’s Lösung):
# Anstelle von globalem Zustand, verwende Fixtures
import pytest
@pytest.fixture
def sample_road():
"""Fixture die Test-Straßendaten bereitstellt."""
return generate_road_profile(num_points=100, x_max=80)
def test_road_profile_length(sample_road):
"""Jeder Test bekommt frische Straßendaten."""
x_road, y_road = sample_road
assert len(x_road) == 100
def test_road_profile_x_values(sample_road):
"""Unabhängig von anderen Tests."""
x_road, y_road = sample_road
assert np.max(x_road) <= 80
Oder einfach: Erstelle Daten in jedem Test
# Einfacher Ansatz - erstelle einfach was du brauchst
def test_road_profile_length():
x_road, y_road = generate_road_profile(num_points=100)
assert len(x_road) == 100
def test_road_profile_x_values():
x_road, y_road = generate_road_profile(num_points=100, x_max=80)
assert np.max(x_road) <= 80
“Aber das ist duplizierter Setup-Code!”
Das ist OK! Vorteile überwiegen die Duplizierung:
- ✅ Tests sind unabhängig und zuverlässig
- ✅ Tests sind selbstdokumentierend (alle Setups sichtbar)
- ✅ Tests können in jeder Reihenfolge laufen
- ✅ Einfach jeden Test isoliert zu verstehen
Wenn Setup wirklich komplex ist, verwende Fixtures. Ansonsten halte es einfach.
Kernaussage
Test-Isolation = Test-Zuverlässigkeit
Jeder Test sollte:
- ✅ Seine eigenen Daten einrichten
- ✅ Unabhängig laufen
- ✅ Aufräumen nach sich selbst (wenn nötig)
- ✅ Andere Tests nicht beeinflussen
Vorteile:
- Kann Tests in jeder Reihenfolge ausführen
- Kann Tests parallel ausführen (schnell!)
- Kann Tests isoliert debuggen
- Fehler sind immer bedeutungsvoll
Denk dran: Instabile Tests (manchmal bestehen, manchmal fehlschlagen) werden normalerweise durch mangelnde Isolation verursacht.
Zusammenfassung und Kernprinzipien
Was du gelernt hast
Nach Abschluss dieser Übungen solltest du erkennen können:
✅ Beschreibende Testnamen - test_[funktion]_[szenario]_[erwartet]()
✅ AAA Pattern - Klare Arrange-Act-Assert Struktur
✅ Ein Konzept pro Test - Fokussiert, klarer Zweck
✅ Angemessener Gebrauch mehrerer Asserts - Gleiches Konzept, verschiedene Aspekte
✅ Test-Isolation - Unabhängig, zuverlässige Tests
Die fünf Prinzipien guter Tests
- Beschreibende Namen
- Muster:
test_[funktion]_[szenario]_[erwartetes_verhalten]() - Warum: Fehlschlagende Tests sagen dir sofort, was kaputt ist
- Muster:
- AAA Struktur
- Arrange → Act → Assert
- Warum: Lesbar, debugbar, wartbar
- Ein Konzept pro Test
- Jeder Test verifiziert ein Verhalten
- Warum: Klare Fehler, unabhängiges Testen
- Intelligenter Gebrauch mehrerer Asserts
- Mehrere Asserts OK für gleiches Konzept
- Tests aufteilen für verschiedene Konzepte
- Warum: Balance zwischen Fokus und Praktikabilität
- Test-Isolation
- Kein geteilter Zustand zwischen Tests
- Jeder Test richtet seine eigenen Daten ein
- Warum: Zuverlässig, parallelisierbar, debugbar
Vorher vs Nachher Auswirkung
Vorher (Schlechte Qualität Tests):
- ❌ Testfehler sind mysteriös
- ❌ Tests schlagen zufällig fehl (Reihenfolge-Abhängigkeit)
- ❌ Debugging dauert Stunden
- ❌ Tests sind schwer zu warten
- ❌ Entwickler vermeiden es, Tests auszuführen
Nachher (Gute Qualität Tests):
- ✅ Testfehler sind sofort klar
- ✅ Tests bestehen/schlagen konsistent fehl
- ✅ Debugging dauert Minuten
- ✅ Tests sind einfach zu warten
- ✅ Entwickler führen Tests häufig aus
Checkliste für das Schreiben von Tests
Verwende diese Checkliste für jeden Test, den du schreibst:
- Name: Beschreibt der Testname klar, was getestet wird?
- Docstring: Erklärt er den Zweck des Tests?
- AAA: Ist die Arrange-Act-Assert Struktur klar?
- Fokus: Testet er genau ein Konzept?
- Asserts: Testen mehrere Asserts dasselbe Konzept?
- Isolation: Hängt dieser Test von anderen Tests ab?
- Klarheit: Werde ich diesen Test in 6 Monaten verstehen?
Übungsaufgabe
Du bist dran!
Nimm diesen schlecht geschriebenen Test und verbessere ihn:
def test_stuff():
data = [5, 15, 25, 35]
result = double_values_above_threshold(data, 20)
assert result == [50, 70] and len(result) == 2
Zu machende Verbesserungen:
- Gib ihm einen beschreibenden Namen
- Füge AAA Struktur hinzu
- Teile mehrere Konzepte auf wenn nötig
- Füge einen Docstring hinzu
- Mache Assertions klar
💡 Verbesserte Version anzeigen
def test_double_values_above_threshold_filters_and_doubles_values():
"""
Test dass double_values_above_threshold korrekt Werte über
dem Schwellenwert filtert und verdoppelt.
"""
# Arrange: Eingabedaten mit Werten über und unter Schwellenwert einrichten
input_data = [5, 15, 25, 35]
threshold = 20
# Act: Daten verarbeiten
result = double_values_above_threshold(input_data, threshold)
# Assert: Korrekte Werte sind verdoppelt verifizieren
expected = [50, 70] # 25*2=50, 35*2=70 (5 und 15 herausgefiltert)
assert result == expected
def test_double_values_above_threshold_returns_correct_count():
"""Test dass korrekte Anzahl Werte den Schwellenwert passiert."""
# Arrange
input_data = [5, 15, 25, 35]
threshold = 20
# Act
result = double_values_above_threshold(input_data, threshold)
# Assert
assert len(result) == 2
Gemachte Verbesserungen:
- ✅ Beschreibender Name, der Funktion + Verhalten erklärt
- ✅ Klare AAA Struktur mit Kommentaren
- ✅ In zwei Tests aufgeteilt (Werte vs. Anzahl sind verschiedene Konzepte)
- ✅ Docstrings hinzugefügt
- ✅ Klare Assertions mit separat definiertem erwarteten Wert
Nächste Schritte
Weiter üben
- Überprüfe deine eigenen Tests - Wende diese Prinzipien auf Tests an, die du geschrieben hast
- Code Review Praxis - Überprüfe Tests von Kommilitonen mit diesen Prinzipien
- Lies pytest Docs - Lerne über Fixtures und Parametrize für fortgeschrittene Techniken
- Schreibe Tests zuerst - Bevor du einen Bug fixst, schreibe einen Test, der ihn reproduziert
Was kommt
Kapitel 03 (Grenzwertanalyse):
- ✅ Äquivalenzklassen-Partitionierung
- ✅ Grenzwertanalyse
- ✅ LLM-unterstütztes Test-Schreiben
- ✅ Parametrisierte Tests mit pytest
Kapitel 03 (TDD und CI):
- ✅ Test-Driven Development (TDD)
- ✅ Test Coverage Metriken
- ✅ Tests zur CI/CD Pipeline hinzufügen
Ressourcen
- pytest Dokumentation
- Clean Code von Robert C. Martin (Kapitel über Unit Tests)
- Test-Driven Development von Kent Beck
Feedback
Fandest du diese Übungen hilfreich? Hast du Vorschläge? Melde dich in der Sprechstunde oder über das Kursforum.
Denk dran: Gute Tests zu schreiben ist eine Fähigkeit, die sich mit Übung verbessert. Jeder Test, den du schreibst, ist eine Gelegenheit, diese Prinzipien anzuwenden!
Viel Erfolg beim Testen! 🧪✨