Home

03 Übung: Test-Qualität Vergleich

exercises chapter-03 testing pytest unit-testing clean-code comparison

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:

Anleitung:

  1. Studiere sowohl die “Schlechte Qualität” als auch “Gute Qualität” Versionen
  2. Identifiziere ALLE Unterschiede zwischen den beiden Versionen
  3. Verstehe, welche Clean Code Prinzipien angewendet werden
  4. Überlege, wie jede Verbesserung Tests wartbarer macht
  5. 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

  1. Was sind die Hauptunterschiede zwischen Schlechter und Guter Qualität?
  2. Was passiert, wenn Tests fehlschlagen in jeder Version?
  3. 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]()

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

  1. Beschreibende Testnamen
    • Muster: test_[funktion]_[szenario]_[erwartet]()
    • Kommuniziert: Was + Wie + Erwartetes Ergebnis
  2. Selbstdokumentierende Tests
    • Testname + Docstring machen den Zweck des Tests kristallklar
    • Zukünftige Entwickler (oder du in 6 Monaten) verstehen sofort
  3. 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

  1. Welche strukturellen Unterschiede machen die Gute Version einfacher zu lesen?
  2. Warum ist die Trennung von Arrange-Act-Assert wichtig?
  3. 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

  1. Was passiert, wenn test_find_intersection_everything() fehlschlägt?
  2. Was sind die Vorteile von aufgeteilten Tests?
  3. 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:

  1. Führe test_find_intersection_everything() aus
  2. Es schlägt fehl (aber du weißt nicht warum, bis du den ganzen Test liest)
  3. Debugge und fixe
  4. Führe gesamten Test erneut aus
  5. Wiederhole

Mit Guter Qualität:

  1. Führe pytest -k "empty_arrays" aus
  2. Test schlägt fehl mit klarer Meldung: “empty arrays test failed”
  3. Debugge und fixe
  4. Führe nur diesen Test erneut aus (schnell!)
  5. 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

  1. Warum ist Beispiel A unangemessen?
  2. Warum ist Beispiel B’s Gebrauch mehrerer Asserts akzeptabel?
  3. 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

  1. Was passiert, wenn Tests in unterschiedlicher Reihenfolge laufen?
  2. Was sind die Probleme mit globalem Zustand?
  3. 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

  1. Beschreibende Namen
    • Muster: test_[funktion]_[szenario]_[erwartetes_verhalten]()
    • Warum: Fehlschlagende Tests sagen dir sofort, was kaputt ist
  2. AAA Struktur
    • Arrange → Act → Assert
    • Warum: Lesbar, debugbar, wartbar
  3. Ein Konzept pro Test
    • Jeder Test verifiziert ein Verhalten
    • Warum: Klare Fehler, unabhängiges Testen
  4. Intelligenter Gebrauch mehrerer Asserts
    • Mehrere Asserts OK für gleiches Konzept
    • Tests aufteilen für verschiedene Konzepte
    • Warum: Balance zwischen Fokus und Praktikabilität
  5. 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):

Nachher (Gute Qualität Tests):

Checkliste für das Schreiben von Tests

Verwende diese Checkliste für jeden Test, den du schreibst:


Ü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:

  1. Gib ihm einen beschreibenden Namen
  2. Füge AAA Struktur hinzu
  3. Teile mehrere Konzepte auf wenn nötig
  4. Füge einen Docstring hinzu
  5. 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:

  1. ✅ Beschreibender Name, der Funktion + Verhalten erklärt
  2. ✅ Klare AAA Struktur mit Kommentaren
  3. ✅ In zwei Tests aufgeteilt (Werte vs. Anzahl sind verschiedene Konzepte)
  4. ✅ Docstrings hinzugefügt
  5. ✅ Klare Assertions mit separat definiertem erwarteten Wert

Nächste Schritte

Weiter üben

  1. Überprüfe deine eigenen Tests - Wende diese Prinzipien auf Tests an, die du geschrieben hast
  2. Code Review Praxis - Überprüfe Tests von Kommilitonen mit diesen Prinzipien
  3. Lies pytest Docs - Lerne über Fixtures und Parametrize für fortgeschrittene Techniken
  4. Schreibe Tests zuerst - Bevor du einen Bug fixst, schreibe einen Test, der ihn reproduziert

Was kommt

Kapitel 03 (Grenzwertanalyse):

Kapitel 03 (TDD und CI):

Ressourcen


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! 🧪✨

© 2026 Dominik Mueller   •  Powered by Soopr   •  Theme  Moonwalk