Home

03 Grundlagen des Testens: Testtheorie, Coverage und Anforderungen

lecture testing coverage test-theory white-box black-box branch-coverage requirements test-adequacy

1. Einleitung: Ihre Tests laufen durch, aber sind sie ausreichend?

Wo Sie bisher stehen:

In Kapitel 03 (Grundlagen des Testens) haben Sie gelernt, wie man Unit-Tests mit pytest schreibt, Tests mit Äquivalenzklassen und Grenzwertanalyse entwirft und die Testpyramide versteht (70% Unit, 20% Modul, 10% E2E).

In Kapitel 03 (TDD und CI) haben Sie gelernt, wie man Testabdeckungsmessung zu Ihrer CI-Pipeline mit pytest-cov hinzufügt, und Sie haben gelernt, dass Coverage ≠ Korrektheit. Sie haben gesehen, dass 70-80% Abdeckung ein gutes Ziel ist, aber Sie haben auch gelernt, dass hohe Abdeckung keine fehlerfreie Software garantiert.

Aber hier ist die Lücke:

Sie wissen wie man Coverage misst (pytest --cov ausführen), aber Sie wissen nicht:

Betrachte dieses Szenario:

Sie arbeiten am Road Profile Viewer Projekt. Das geometry.py Modul hat eine Funktion find_intersection(), die berechnet, wo ein Kamerastrahl ein Straßenprofil schneidet. Sie führen die Tests aus:

$ pytest tests/test_geometry.py -v
==== test session starts ====
tests/test_geometry.py::test_find_intersection_finds_intersection_for_normal_angle PASSED [100%]

==== 1 passed in 0.15s ====

✅ Alle Tests bestehen! Ab in die Produktion, oder?

Dann überprüfen Sie die Coverage:

$ pytest tests/test_geometry.py --cov=road_profile_viewer.geometry --cov-report=term-missing
==== test session starts ====
tests/test_geometry.py::test_find_intersection_finds_intersection_for_normal_angle PASSED [100%]

---------- coverage: platform win32, python 3.12.0 -----------
Name                                  Stmts   Miss  Cover   Missing
-------------------------------------------------------------------
road_profile_viewer\geometry.py          45     18    60%   45, 98, 109-110, 123-127, 138
-------------------------------------------------------------------

Nur 60% Coverage! Und Sie haben 18 fehlende Zeilen - Zeilen, die nie von Ihrer Testsuite ausgeführt wurden.

Die heutige Frage: Wie entwerfen Sie systematisch Tests, um diese fehlenden Zeilen abzudecken? Noch wichtiger: Sollten Sie sie abdecken, und was sagt Ihnen Coverage über die Qualität Ihrer Tests?


2. Lernziele

Am Ende dieser Vorlesung werden Sie:

  1. Das theoretische Framework verstehen für Testing: Programm, Eingabebereich, Testsuite, Modell und Coverage-Kriterium
  2. Verschiedene Arten von Code-Coverage erklären: Statement (C0) und Branch (C1) Coverage
  3. Systematisch Tests entwerfen, um bestimmte Coverage-Ziele zu erreichen (z.B. 100% Branch Coverage)
  4. Erkennen, dass Äquivalenzklassen und Grenzwerte auch Coverage-Kriterien über ein Eingabebereichs-Modell sind
  5. Die Grenzen von Coverage-Metriken verstehen
  6. Eine iterative Spirale zwischen Anforderungen, Tests und Coverage anwenden, um beide zu verfeinern

Was Sie LERNEN wirst:

Was Sie NICHT lernen wirst:


3. Die Testtheorie: Programm, Modell, Testsuite, Coverage

Bevor wir in den Code eintauchen, lassen Sie uns ein theoretisches Vokabular etablieren. Dieses Framework wird Ihnen helfen, für den Rest Ihrer Karriere klar über Testing und Coverage nachzudenken.

3.1 Grundlegende Definitionen

Verbindung zu Kapitel 03 (Grundlagen des Testens): Das unendliche Eingabeproblem

In Kapitel 03 (Grundlagen des Testens) haben Sie Äquivalenzklassen als praktische Technik kennengelernt, um mit unendlichen Eingabebereichen umzugehen. Erinnern Sie sich an das reciprocal_sum() Beispiel? Sie konnten nicht alle möglichen Kombinationen von (x, y, z) Werten testen, also haben Sie den Eingaberaum in Äquivalenzklassen partitioniert (positive Summe, negative Summe, null Summe) und einen Repräsentanten aus jeder getestet.

Das war Test-Design-Denken. Jetzt gehen wir eine Ebene tiefer: Wie formalisieren wir das? Wie messen wir, ob unsere Tests “gut genug” sind? Wie vergleichen wir verschiedene Teststrategien?

Hier kommt die Testtheorie ins Spiel. Lassen Sie uns das formale Vokabular aufbauen, beginnend mit einem einfachen Beispiel.

Beispiel: Eine einfache Funktion zum Einstieg

Bevor wir find_intersection() angehen, beginnen wir mit etwas Trivialem:

def classify_number(x: float) -> str:
    """Classify a number as positive, negative, or zero."""
    if x > 0:
        return "positive"
    elif x < 0:
        return "negative"
    else:
        return "zero"

Diese Funktion hat diskrete Ausgabewerte (nur 3 mögliche Strings) und klare Entscheidungslogik (perfekt zum Lernen der Theorie).

Jetzt definieren wir die formalen Begriffe anhand dieses Beispiels:

Programm \(P\)

Das Software-Artefakt, das Sie testen.

Eingabebereich \(D\)

Die Menge aller möglichen Eingaben für das Programm.

Für classify_number():

\(D = \mathbb{R}\) (alle reellen Zahlen)

Für find_intersection():

\(D = \{(x_{road}, y_{road}, angle, camera_x, camera_y) \lvert x_{road}, y_{road} \in \mathbb{R}^n, angle \in \mathbb{R}, camera_x, camera_y \in \mathbb{R}\}\)

Beide sind unendlich! Sie können nicht jede mögliche Eingabe testen. Das ist genau das Problem, das Kapitel 03 (Grundlagen des Testens)’s Äquivalenzklassen adressiert haben.

Testfall \(t\)

Ein einzelnes Element aus dem Eingabebereich: \(t \in D\)

Für classify_number(): \(t = 5.0\) oder \(t = -3.14\) oder \(t = 0.0\)

Für find_intersection(): t = (np.array([0, 10, 20]), np.array([0, 2, 4]), 10.0, 0.0, 10.0)

Testsuite \(T\)

Eine endliche Teilmenge des Eingabebereichs: \(T \subseteq D\)

Ihre Testsuite ist die Sammlung aller Testfälle, die Sie tatsächlich ausführst.

Für classify_number() könnte eine gute Testsuite sein:

\(T = \{5.0, -3.14, 0.0\}\) → \(\vert T \vert = 3\)

Für find_intersection() haben Sie derzeit:

\(\vert T \vert = 1\) (nur ein Test!)

Zurück zu Äquivalenzklassen

Beachte, wie \(T = \{5.0, -3.14, 0.0\}\) für classify_number() direkt auf Äquivalenzklassen aus Kapitel 03 (Grundlagen des Testens) abbildet:

Äquivalenzklassen sind eine Möglichkeit, systematisch ein endliches \(T\) aus einem unendlichen \(D\) zu wählen. Aber sie sind nicht die einzige! Das erkunden wir in dieser Vorlesung.

3.2 Modell eines Programms

Hier ist die entscheidende Einsicht: Coverage ist immer Coverage eines Modells.

Ein Modell \(M(P)\) ist eine Abstraktion des Programmverhaltens. Aber was genau ist \(M(P)\)? Ist es eine Funktion? Eine Menge? Ein Graph?

Antwort: Es kommt auf den Modelltyp an! Verschiedene Modelle repräsentieren Programme als verschiedene mathematische Objekte.

Strukturelle Modelle (White-Box)

Diese Modelle werden von der Quellcodestruktur des Programms abgeleitet:

Semantische Modelle (Black-Box)

Diese Modelle werden vom beabsichtigten Verhalten des Programms abgeleitet:

Warum ist das wichtig? Weil \(M(P)\) die Grundlage für die Definition von Testanforderungen \(R_C(P)\) ist. Schauen wir uns konkrete Beispiele an.

Beispiel 1: CFG für classify_number() - Das vollständige Modell

Ein Control Flow Graph (CFG) ist eine grafische Darstellung aller Pfade, die Sierch ein Programm ausgeführt werden könnten. Jeder Knoten repräsentiert eine Anweisung oder Entscheidung, und jede Kante repräsentiert Kontrollfluss.

Das Modell \(M_{CFG}(\text{classify_number})\):

Für das CFG-Modell ist \(M(P)\) ein gerichteter Graph \(G = (V, E)\) wobei:

\(V = \{\text{Start}, \text{Check1}, \text{Check2}, \text{Positive}, \text{Negative}, \text{Zero}, \text{End}\}\)

\(E = \{(\text{Start}, \text{Check1}), (\text{Check1}, \text{Positive}), (\text{Check1}, \text{Check2}), (\text{Check2}, \text{Negative}), (\text{Check2}, \text{Zero}), (\text{Positive}, \text{End}), (\text{Negative}, \text{End}), (\text{Zero}, \text{End})\}\)

Das ist, was M(P) tatsächlich ist - eine konkrete Menge von Knoten und Kanten!

Hier ist die visuelle Darstellung von \(M_{CFG}(\text{classify_number})\):

graph TD
    Start([Start]) --> Check1{x > 0?}
    Check1 -->|True| Positive[Return 'positive']
    Check1 -->|False| Check2{x < 0?}
    Check2 -->|True| Negative[Return 'negative']
    Check2 -->|False| Zero[Return 'zero']
    Positive --> End([End])
    Negative --> End
    Zero --> End

Vom Modell zu Testanforderungen:

Nun, da wir \(M_{CFG}(\text{classify_number})\) haben, können wir verschiedene Coverage-Kriterien definieren. Schauen wir uns eine Vorschau auf zwei gängige an (wir definieren sie präzise in Abschnitt 4):

Statement Coverage (C₀):

\(R_{C_0}(P) = V \setminus \{\text{Start}, \text{End}\} = \{\text{Check1}, \text{Check2}, \text{Positive}, \text{Negative}, \text{Zero}\}\)

Jede Anforderung bedeutet “führe diesen Knoten mindestens einmal aus.” Also \(\vert R_{C_0}(P) \vert = 5\) Testanforderungen.

Branch Coverage (C₁):

\(R_{C_1}(P) = E = \{(\text{Start}, \text{Check1}), (\text{Check1}, \text{Positive}), (\text{Check1}, \text{Check2}), …\}\)

Jede Anforderung bedeutet “traversiere diese Kante mindestens einmal.” Also \(\vert R_{C_1}(P) \vert = 8\) Testanforderungen.

Konzentrieren Sie sich vorerst darauf zu verstehen, wie \(R_C(P)\) von \(M(P)\) abgeleitet wird - die spezifischen Bedeutungen von C₀ und C₁ werden in Abschnitt 4 klar.

Wichtige Einsicht: Es gibt 3 mögliche Ausführungspfade durch diesen CFG:

  1. \(x > 0\) → return “positive” (führt Kanten aus: Start→Check1, Check1→Positive, Positive→End)
  2. \(x \leq 0\) und \(x < 0\) → return “negative” (führt Kanten aus: Start→Check1, Check1→Check2, Check2→Negative, Negative→End)
  3. \(x \leq 0\) und \(x \geq 0\) → return “zero” (führt Kanten aus: Start→Check1, Check1→Check2, Check2→Zero, Zero→End)

Unsere Testsuite \(T = \{5.0, -3.14, 0.0\}\) führt alle 3 Pfade aus und deckt alle 8 Kanten ab! Das ist 100% Branch Coverage des CFG.

Ist \(M(P)\) wichtig für Studierende? JA! Zu verstehen, dass:

…hilft Ihnen zu verstehen, WARUM verschiedene Coverage-Kriterien existieren und wie man systematisch Tests entwirft.

Beispiel 2: CFG für find_intersection() (Komplexität der realen Welt)

Jetzt sehen wir, warum das einfache Beispiel notwendig war. Hier ist der CFG für die tatsächliche find_intersection() Funktion, die Sie testen:

graph TD
    Start([Start]) --> B1{cos angle ≈ 0?}
    B1 -->|True| R1[Return None]
    B1 -->|False| Loop[For each segment]
    Loop --> B2{Segment behind<br/>camera?}
    B2 -->|True| Loop
    B2 -->|False| B3{Ray crosses<br/>segment?}
    B3 -->|False| Loop
    B3 -->|True| B4{Parallel lines?}
    B4 -->|True| SetT[Set t=0]
    B4 -->|False| SetT
    SetT --> Calc[Calculate intersection]
    Calc --> R2[Return intersection]
    Loop -->|No more segments| R3[Return None]
    R1 --> End([End])
    R2 --> End
    R3 --> End

Beachte den Unterschied in der Komplexität! find_intersection() hat:

Ihr einzelner existierender Test erkundet nur einen Pfad durch diesen CFG. Viele Branches bleiben ungetestet!

Beispiel 3: Call Graph (Für Multi-Funktions-Programme)

Ein Call Graph zeigt, welche Funktionen welche anderen Funktionen aufrufen. Zum Beispiel, wenn Sie hättest:

def main():
    data = load_data()
    result = process(data)
    save_result(result)

Der Call Graph wäre:

graph LR
    main[main] -->|calls| load[load_data]
    main -->|calls| proc[process]
    main -->|calls| save[save_result]

Coverage-Kriterien können auch hierauf abzielen: “Haben Sie alle Funktionsaufrufe getestet?” Für dieses einfache Beispiel bräuchten Sie Tests, die sicherstellen, dass main() tatsächlich alle drei Hilfsfunktionen aufruft.

Beispiel 4: Data Flow Graph (Variablenverwendung verfolgen)

Ein Data Flow Graph verfolgt, wie Daten durch Variablen fließen. Betrachte:

def compute(x):
    y = x * 2      # Definition von y
    z = y + 1      # Verwendung von y, Definition von z
    return z       # Verwendung von z

Data Flow Coverage fragt: “Haben Sie alle Definition-Use-Paare getestet?” Zum Beispiel:

Zusammenfassung: Warum Modelle wichtig sind

Verschiedene Modelle führen zu verschiedenen Coverage-Kriterien:

Sie wählen das Modell basierend darauf, welche Bugs Sie finden willst!

3.3 Coverage-Kriterium und Testanforderungen

Ein Coverage-Kriterium \(C\) ist eine Regel, die eine endliche Menge von Testanforderungen (auch Testverpflichtungen genannt) definiert:

\(R_C(P) = \{r_1, r_2, …, r_n\}\)

Jede Anforderung \(r_i\) ist etwas, das von Ihrer Testsuite “abgedeckt” werden sollte.

Ein Testfall \(t \in D\) erfüllt eine Anforderung \(r \in R_C(P)\), wenn die Ausführung von \(P\) auf \(t\) das Verhalten zeigt, das \(r\) beschreibt. Zum Beispiel:

Beispiel: Testanforderungen für find_intersection()

Wenn wir Statement Coverage als unser Kriterium \(C_{stmt}\) verwenden, dann:

\(R_{C_{stmt}}(\text{find_intersection}) = \lbrace\text{stmt}_1, \text{stmt}_2, …, \text{stmt}_{22}\rbrace\)

Jede Anforderung \(\text{stmt}_i\) bedeutet “führe Anweisung \(i\) mindestens einmal aus.”

Unser einzelner existierender Test erfüllt ungefähr 60% dieser Anforderungen (nur ~13 von 22 Anweisungen).

3.4 Das Adequacy Framework

Jetzt formalisieren wir Coverage-Messung und Adäquanz.

Coverage einer Testsuite

Die Coverage einer Testsuite \(T\) in Bezug auf Kriterium \(C\) ist die Menge der Anforderungen, die von \(T\) erfüllt werden:

\(\text{cov}_C(T, P) = \{ r \in R_C(P) \lvert \exists t \in T \text{ sodass } t \text{ erfüllt } r \}\)

Der Coverage-Prozentsatz ist:

\(\frac{\vert\text{cov}_C(T, P)\vert}{\vert R_C(P)\vert} \times 100\%\)

Test-Adäquanz

Eine Testsuite \(T\) ist adäquat in Bezug auf Kriterium \(C\) genau dann, wenn:

\(\text{cov}_C(T, P) = R_C(P)\)

Mit anderen Worten, \(T\) ist adäquat, wenn es alle Anforderungen erfüllt. Dies entspricht “100% Coverage.”

Beispiel: Adäquanz für find_intersection()

Derzeit:

Coverage: \(\frac{13}{22} \times 100\% \approx 59\%\)

\(T\) ist NICHT adäquat für \(C_{stmt}\) weil \(\text{cov}_{C_{stmt}}(T, P) \neq R_{C_{stmt}}(P)\).

Eigenschaften guter Coverage-Kriterien

Ein nützliches Coverage-Kriterium sollte diese Eigenschaften haben:

1. Endlichkeit

\(R_C(P)\) muss endlich sein, sonst könnten Sie niemals Adäquanz erreichen.

2. Monotonie

Wenn \(T \subseteq T’\), dann \(\text{cov}_C(T, P) \subseteq \text{cov}_C(T’, P)\).

Tests hinzufügen kann Coverage nicht reduzieren. Das macht intuitiv Sinn!

Beispiel:

3. Nicht-Trivialität

Wir wollen nicht \(R_C(P) = \emptyset\) (das würde Adäquanz bedeutungslos machen).

Jedes Programm sollte mindestens einige Anforderungen zum Erfüllen haben.

Weitere Überlegungen:

3.5 Subsumption: Coverage-Kriterien vergleichen

Gegeben zwei Coverage-Kriterien \(C_1\) und \(C_2\), sagen wir:

\(C_1\) subsumiert \(C_2\) genau dann, wenn: Für jedes Programm \(P\) und jede Testsuite \(T\): Wenn \(T\) adäquat für \(C_1\) ist, dann ist \(T\) auch adäquat für \(C_2\).

Was bedeutet Subsumption in der Praxis?

Wenn \(C_1\) \(C_2\) subsumiert, dann:

Subsumptionshierarchie (Vorschau):

Path Coverage
     ↓ subsumiert
Branch Coverage (C1)
     ↓ subsumiert
Statement Coverage (C0)
     ↓ subsumiert
Function Coverage

Formales Beispiel: Branch subsumiert Statement

Beweisen wir, dass Branch Coverage (\(C_1\)) Statement Coverage (\(C_0\)) für find_intersection() subsumiert.

Beweisskizze:

Betrachte den CFG \(G = (V, E)\) wobei:

Statement Coverage-Anforderungen: \(R_{C_0}(P) = V\)

Jede Anforderung ist “führe Knoten \(v\) mindestens einmal aus.”

Branch Coverage-Anforderungen: \(R_{C_1}(P) = E\)

Jede Anforderung ist “traversiere Kante \(e\) mindestens einmal.”

Wichtige Beobachtung: Jede Kante \(e = (v_1, v_2) \in E\) hat:

Wenn Sie Kante \(e\) traversierst, müssen Sie sowohl \(v_1\) als auch \(v_2\) ausführen.

Daher: Wenn eine Testsuite \(T\) alle Kanten \(E\) abdeckt, muss sie alle Knoten \(V\) abdecken.

Formal: Wenn \(\text{cov}{C_1}(T, P) = E\), dann \(\text{cov}{C_0}(T, P) = V\).

Schlussfolgerung: \(C_1\) (Branch) subsumiert \(C_0\) (Statement). ✅

Beispiel mit find_intersection():

Angenommen, Sie erreichen 100% Branch Coverage:

Also gibt Ihnen 100% Branch Coverage automatisch 100% Statement Coverage für diese Anweisungen!

Aber das Umgekehrte ist NICHT wahr!

Sie könnten 100% Statement Coverage ohne 100% Branch Coverage haben:

Beispiel schlechte Testsuite:

def test_only_normal_case():
    # Testet nur den "False" Branch jeder Entscheidung
    x, y, dist = find_intersection(
        np.array([0, 10, 20]),
        np.array([0, 2, 4]),
        10.0, 0.0, 10.0
    )
    assert x is not None

Dieser Test könnte viele Anweisungen ausführen (hohe C0), aber er testet niemals:

Also: Hohe C0 ≠ Hohe C1, aber Hohe C1 → Hohe C0.

Warum Subsumption wichtig ist:

  1. Leitet Kriterienauswahl: Wähle stärkere Kriterien, wenn Sie sie sich leisten können
  2. Vermeide redundante Arbeit: Messe nicht sowohl C1 als auch C0 (C1 ist ausreichend)
  3. Theoretische Grundlagen: Hilft, verschiedene Teststrategien formal zu vergleichen

Weitere Subsumptionsbeziehungen (zukünftige Themen):

Verbindung zu Kapitel 03 (Grundlagen des Testens):

Erinnern Sie sich, in Kapitel 03 (Grundlagen des Testens) haben Sie Äquivalenzklassen als Input-Domain-Partitionierungs-Technik kennengelernt. Equivalence Class Coverage ist auch ein Coverage-Kriterium:

\(R_{EC}(P) = \lbrace EC_1, EC_2, …, EC_n\rbrace\)

Jede Anforderung ist “teste mindestens einen Repräsentanten aus Äquivalenzklasse \(EC_i\).”

Aber Equivalence Class Coverage und Branch Coverage sind unvergleichbar (keines subsumiert das andere)! Sie brauchen beides:

Wir werden das detailliert in Abschnitt 6 sehen.


4. Code Coverage: Statement (C0) und Branch (C1)

Jetzt machen wir das konkret mit den zwei häufigsten Coverage-Kriterien.

4.1 Statement Coverage (C0)

Modell: Control Flow Graph (Knoten = Anweisungen)

Testanforderungen: Jede Anweisung mindestens einmal ausführen

\(R_{C0}(P) = \{\text{statement}_1, \text{statement}_2, …, \text{statement}_n\}\)

Beispiel für find_intersection():

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]:
    angle_rad = -np.deg2rad(angle_degrees)              # Anweisung 1

    if np.abs(np.cos(angle_rad)) < 1e-10:               # Anweisung 2 (Entscheidung)
        return None, None, None                         # Anweisung 3

    slope = np.tan(angle_rad)                           # Anweisung 4

    for i in range(len(x_road) - 1):                    # Anweisung 5 (Schleife)
        x1, y1 = x_road[i], y_road[i]                   # Anweisung 6
        x2, y2 = x_road[i + 1], y_road[i + 1]           # Anweisung 7

        if x2 <= camera_x:                              # Anweisung 8 (Entscheidung)
            continue                                    # Anweisung 9

        ray_y1 = camera_y + slope * (x1 - camera_x)     # Anweisung 10
        ray_y2 = camera_y + slope * (x2 - camera_x)     # Anweisung 11

        diff1 = ray_y1 - y1                             # Anweisung 12
        diff2 = ray_y2 - y2                             # Anweisung 13

        if diff1 * diff2 <= 0:                          # Anweisung 14 (Entscheidung)
            if abs(diff2 - diff1) < 1e-10:              # Anweisung 15 (Entscheidung)
                t = 0                                   # Anweisung 16
            else:
                t = diff1 / (diff1 - diff2)             # Anweisung 17

            x_intersect = x1 + t * (x2 - x1)            # Anweisung 18
            y_intersect = y1 + t * (y2 - y1)            # Anweisung 19

            distance = np.sqrt(...)                     # Anweisung 20

            return x_intersect, y_intersect, distance   # Anweisung 21

    return None, None, None                             # Anweisung 22

Statement Coverage erfordert die Ausführung aller 22 Anweisungen mindestens einmal.

Was bedeutet 60% Coverage?

Aus dem Coverage-Report von vorhin: 45 statements, 18 missed = 60%

Das bedeutet 18 Anweisungen (wie Anweisungen 3, 9, 16, 22 oben) wurden nie ausgeführt von Ihrem einzelnen Test.

4.2 Branch Coverage (C1)

Modell: Control Flow Graph (Kanten = Branches von Entscheidungen)

Testanforderungen: Jedes Entscheidungsergebnis (True und False) mindestens einmal ausüben

\(R_{C1}(P) = \{\text{branch}_{1T}, \text{branch}_{1F}, \text{branch}_{2T}, \text{branch}_{2F}, …\}\)

Warum ist das anders als C0?

Betrachte diesen Code:

if condition:
    do_something()
do_next_thing()

Mit C0 müssen Sie nur do_next_thing() ausführen - Sie könnten das if komplett überspringen!

Mit C1 müssen Sie BEIDES testen:

Branch Coverage-Tabelle für find_intersection():

Branch ID Entscheidung True-Pfad False-Pfad Aktuelle Coverage
B1 np.abs(np.cos(angle_rad)) < 1e-10 Vertikaler Strahl → return None Normaler Strahl → weiter ❌ Nur False abgedeckt
B2 x2 <= camera_x Segment hinter Kamera → überspringen Segment vor → Schnittpunkt prüfen ❌ Nur False abgedeckt
B3 diff1 * diff2 <= 0 Strahl kreuzt Segment → Schnittpunkt berechnen Strahl kreuzt nicht → weiter ✅ Beide abgedeckt (Glück!)
B4 abs(diff2 - diff1) < 1e-10 Parallele Linien → t=0 Normaler Schnittpunkt → t berechnen ❌ Nur False abgedeckt
B5 Schleifeniterationen Mindestens eine Iteration Null Iterationen (leeres Array) ❌ Null Iterationen nicht getestet
B6 Return-Pfad nach Schleife Kein Schnittpunkt gefunden → return None Schnittpunkt früher gefunden → return Werte ❌ Kein Schnittpunkt-Pfad nicht getestet

4.3 C1 subsumiert C0

Behauptung: Wenn Sie 100% Branch Coverage erreichen, erreichen Sie automatisch 100% Statement Coverage.

Beweis durch Beispiel:

Um beide Branches von if condition: do_something() abzudecken:

Beide Branches erfordern die Ausführung der if Anweisung selbst ✅

Also wird jede Anweisung ausgeführt. Q.E.D.

Aber das Umgekehrte ist NICHT wahr: Sie können 100% C0 ohne 100% C1 haben (indem Sie nur einen Branch jeder Entscheidung testen).


5. Systematisches Test-Design für Branch Coverage

Jetzt kommt der praktische Teil: Wie entwerfen Sie systematisch Tests, um 100% C1 Coverage für find_intersection() zu erreichen?

5.1 Schritt-für-Schritt-Methodologie

Schritt 1: Alle Entscheidungspunkte identifizieren

Gehe durch den Code und liste jedes if, for, while und jede Entscheidung auf:

  1. if np.abs(np.cos(angle_rad)) < 1e-10 (vertikaler Strahl Check)
  2. for i in range(len(x_road) - 1) (Schleife - 0 vs 1+ Iterationen)
  3. if x2 <= camera_x (hinter Kamera Check)
  4. if diff1 * diff2 <= 0 (Schnittpunkt Check)
  5. if abs(diff2 - diff1) < 1e-10 (parallel Check)
  6. Return-Pfade (früher Return vs Schleifenabschluss)

Schritt 2: Für jede Entscheidung, Eingaben für BEIDE Ergebnisse entwerfen

Verwende Ihren CFG oder mentale Nachverfolgung, um herauszufinden:

Schritt 3: Schleifenabdeckung prüfen

Schritt 4: Alle Return-Pfade prüfen

5.2 Durchgearbeitetes Beispiel 1: Vertikaler Strahl Test

Branch: B1 (True-Pfad) - np.abs(np.cos(angle_rad)) < 1e-10

Analyse: Dies ist True, wenn der Strahl vertikal ist (Winkel = 90° oder 270°), weil \(\cos(90°) = 0\).

Test:

import numpy as np
from road_profile_viewer.geometry import find_intersection

def test_vertical_ray_returns_none():
    """
    Test dass find_intersection() None für einen vertikalen Strahl zurückgibt.

    Branch Coverage: B1 True-Pfad
    Äquivalenzklasse: Vertikale Strahlen (cos(angle) ≈ 0)
    """
    # Arrange: Beliebiges Straßenprofil erstellen (egal - wird nicht schneiden)
    x_road = np.array([0, 10, 20], dtype=np.float64)
    y_road = np.array([0, 1, 2], dtype=np.float64)

    # Vertikaler Strahl (nach unten): angle = 90°
    angle = 90.0
    camera_x = 5.0
    camera_y = 10.0

    # Act
    x, y, dist = find_intersection(x_road, y_road, angle, camera_x, camera_y)

    # Assert: Vertikaler Strahl sollte None zurückgeben (by design)
    assert x is None, "Vertikaler Strahl sollte None für x zurückgeben"
    assert y is None, "Vertikaler Strahl sollte None für y zurückgeben"
    assert dist is None, "Vertikaler Strahl sollte None für Distanz zurückgeben"

Coverage gewonnen: ✅ B1 True-Pfad, Anweisung 3

5.3 Durchgearbeitetes Beispiel 2: Straßensegment hinter Kamera

Branch: B2 (True-Pfad) - x2 <= camera_x

Analyse: Dies ist True, wenn die gesamte Straße hinter der Kameraposition ist.

Test:

def test_road_entirely_behind_camera_returns_none():
    """
    Test dass find_intersection() None zurückgibt, wenn alle Straßensegmente
    hinter der Kameraposition sind.

    Branch Coverage: B2 True-Pfad, B6 True-Pfad (Schleife endet ohne Schnittpunkt)
    Äquivalenzklasse: Alle Straßen-x-Koordinaten <= camera_x
    """
    # Arrange: Straße bei x = [0, 5, 10], Kamera bei x = 15
    x_road = np.array([0, 5, 10], dtype=np.float64)
    y_road = np.array([0, 1, 2], dtype=np.float64)

    angle = 45.0  # Nach unten Winkel
    camera_x = 15.0  # Kamera ist bei x=15 (gesamte Straße dahinter bei x <= 10)
    camera_y = 5.0

    # Act
    x, y, dist = find_intersection(x_road, y_road, angle, camera_x, camera_y)

    # Assert: Kein Schnittpunkt, weil Straße komplett hinter Kamera
    assert x is None
    assert y is None
    assert dist is None

Coverage gewonnen: ✅ B2 True-Pfad, ✅ B6 True-Pfad (kein Schnittpunkt gefunden), Anweisung 9, Anweisung 22

5.4 Durchgearbeitetes Beispiel 3: Parallele Linien Fall

Branch: B4 (True-Pfad) - abs(diff2 - diff1) < 1e-10

Analyse: Dies passiert, wenn der Strahl parallel zu einem Straßensegment ist. Das ist ein numerischer Stabilitäts-Grenzfall.

Test:

def test_parallel_ray_to_road_segment():
    """
    Test Schnittpunkt, wenn Strahl parallel zu einem Straßensegment ist.

    Branch Coverage: B4 True-Pfad
    Äquivalenzklasse: Strahlsteigung gleich Straßensegment-Steigung
    Grenzfall: Numerische Stabilität wenn Differenzen nahe Null sind
    """
    # Arrange: Horizontales Straßensegment
    x_road = np.array([0, 10, 20], dtype=np.float64)
    y_road = np.array([5, 5, 3], dtype=np.float64)  # Erstes Segment horizontal bei y=5

    # Horizontaler Strahl bei y=5 (parallel zum ersten Segment)
    angle = 0.0  # Horizontaler Strahl
    camera_x = 5.0
    camera_y = 5.0  # Kamera genau auf der Straßenlinie

    # Act
    x, y, dist = find_intersection(x_road, y_road, angle, camera_x, camera_y)

    # Assert: Sollte Schnittpunkt finden (verwendet t=0 für parallelen Fall)
    # Die Implementierung wählt den Start des Segments
    assert x is not None
    assert y is not None
    assert dist is not None

Coverage gewonnen: ✅ B4 True-Pfad, Anweisung 16

5.5 Studentenübungen

Jetzt sind Sie dran! Entwirf Tests für diese verbleibenden Branches:

Übung 1: Entwirf einen Test für B5 False-Pfad (null Schleifeniterationen)

Hinweis: Was passiert, wenn x_road und y_road Länge 0 oder 1 haben?

Übung 2: Entwirf einen Test für B3 False-Pfad (Strahl kreuzt Segment nicht)

Hinweis: Positioniere die Straße unterhalb der Kamera und richte den Strahl nach oben.

Übung 3: Entwirf einen Test, der einen Schnittpunkt am zweiten Segment findet, nicht am ersten

Hinweis: Lass das erste Segment verfehlen und das zweite Segment treffen.

Übung 4: Entwirf einen Test für einen Grenzfall: Schnittpunkt genau an einem Straßeneckpunkt

Hinweis: Lass den Strahl genau durch x_road[i] gehen.


6. Äquivalenzklassen und Grenzwerte als Coverage-Kriterien

Erinnern Sie sich an Äquivalenzklassen und Grenzwertanalyse aus Kapitel 03 (Grundlagen des Testens)? Sie schienen wie informelle Heuristiken. Aber jetzt haben Sie das theoretische Framework, um sie richtig zu verstehen:

Äquivalenzklassen sind ein Coverage-Kriterium über ein Eingabebereichs-Modell.

6.1 Das Eingabebereichs-Modell

Modell: Partitioniere den Eingabebereich \(D\) in Äquivalenzklassen \(EC_1, EC_2, …, EC_n\)

Wobei:

Testanforderungen:

\(R_{EC}(P) = \lbrace EC_1, EC_2, …, EC_n\rbrace\)

Coverage: Teste mindestens einen repräsentativen Wert aus jeder Äquivalenzklasse.

6.2 Äquivalenzklassen für find_intersection()

EC ID Äquivalenzklasse Beschreibung Repräsentativer Wert Erwartetes Verhalten
EC1 Normaler Schnittpunkt Strahl trifft Straße vor Kamera, einzelner Schnittpunkt angle=10°, Kamera über Straße, Straße vorne Gibt (x, y, dist) mit gültigen Werten zurück
EC2 Kein Schnittpunkt Strahl verfehlt Straße komplett (z.B. Strahl zeigt nach oben) angle=-45°, Straße unter Kamera Gibt (None, None, None) zurück
EC3 Vertikaler Strahl Strahl ist vertikal (angle = 90° oder 270°) angle=90° Gibt (None, None, None) zurück
EC4 Straße hinter Kamera Alle Straßensegmente haben x ≤ camera_x x_road=[0,10], camera_x=20 Gibt (None, None, None) zurück
EC5 Mehrere Schnittpunkte Strahl könnte mehrere Segmente treffen (gebe ersten zurück) Wellige Straße mit Strahl, der mehrere Segmente kreuzt Gibt ersten gefundenen Schnittpunkt zurück
EC6 Schnittpunkt am Eckpunkt Strahl geht genau durch einen Straßeneckpunkt Strahl zielt auf x_road[i], y_road[i] Gibt Eckpunkt-Koordinaten zurück
EC7 Paralleler Strahl und Segment Strahlsteigung gleich Straßensegment-Steigung Horizontaler Strahl, horizontales Straßensegment Gibt Schnittpunkt zurück (verwendet t=0)
EC8 Leere Straße x_road und y_road haben Länge < 2 x_road=[], y_road=[] Gibt (None, None, None) zurück
EC9 Schnittpunkt an Kamera Strahl schneidet Straße genau an Kameraposition camera_x=5, camera_y=2, Straße geht durch (5,2) Gibt (5, 2, 0) zurück - Distanz ist Null

6.3 Grenzwertanalyse

Modell: Identifiziere Grenzen zwischen Äquivalenzklassen

Testanforderungen: Teste Werte an, knapp über und knapp unter jeder Grenze

Grenzen für find_intersection():

Parameter Grenze Testwerte
angle_degrees 0° (horizontal), 90° (vertikal unten), 180°, 270° (vertikal oben) -0.1°, 0°, 0.1°, 89.9°, 90°, 90.1°, ...
camera_y Genau auf Straße (y = y_road[i]) y_road[i] - ε, y_road[i], y_road[i] + ε
camera_x Genau am Straßenstart (x_road[0]) oder Ende (x_road[-1]) x_road[0] - ε, x_road[0], x_road[0] + ε
x_road length Leer (0), einzelner Punkt (1), zwei Punkte (2) len=0, len=1, len=2, len=3
Numerischer Schwellwert np.abs(np.cos(angle_rad)) < 1e-10 Werte nahe 1e-10 (numerische Stabilität)
Numerischer Schwellwert abs(diff2 - diff1) < 1e-10 Werte nahe 1e-10 (Parallelerkennung)

6.4 EC/BVA vs Branch Coverage

Wichtige Einsicht: Equivalence Class Coverage und Branch Coverage sind komplementär, nicht konkurrierend!

Sie brauchen beides!

Beispiel: Sie könnten 100% C1 erreichen, ohne EC9 zu testen (Schnittpunkt an Kameraposition mit Distanz=0). Dies könnte einen Bug in der Distanzberechnung aufdecken, den C1 allein nicht finden würde.

Umgekehrt könnten Sie alle ECs testen, aber den if abs(diff2 - diff1) < 1e-10 Branch verpassen, wenn Ihr paralleler Strahl-Test nicht den exakten numerischen Schwellwert auslöst.


7. Grenzen von Coverage

Jetzt die unbequeme Wahrheit: 100% Coverage bedeutet NICHT fehlerfreie Software.

7.1 Limitation 1: Korrekte Struktur, falsche Logik

Szenario: Sie erreichen 100% C1 Coverage, aber es gibt einen Bug in der Formel.

Beispiel:

# BUG: Falsche Distanzformel (sollte sqrt sein, nicht nur Summe)
distance = (x_intersect - camera_x) + (y_intersect - camera_y)  # ❌ FALSCH!

Test, der trotz Bug besteht:

def test_finds_intersection():
    # ... Testcode, der diese Zeile ausführt ...
    assert dist is not None  # ✅ Besteht (dist ist *eine* Zahl, nur die falsche!)

Was ist schiefgelaufen?

Lektion: Coverage sagt Ihnen, was Sie ausgeführt hast, nicht was Sie verifiziert hast.

7.2 Limitation 2: Fehlende Test-Assertions

Szenario: Ihr Test führt Code aus, aber prüft das Ergebnis nicht.

Beispiel:

def test_weak_assertion():
    # Arrange
    x_road = np.array([0, 10, 20], dtype=np.float64)
    y_road = np.array([0, 2, 4], dtype=np.float64)

    # Act
    x, y, dist = find_intersection(x_road, y_road, 10.0, 0.0, 10.0)

    # Assert: Schwache Assertion!
    assert x is not None  # ❌ Prüft nicht, ob x KORREKT ist!

Coverage-Report: 100% C1 ✅

Bug-Erkennung: 0% - dieser Test würde bestehen, selbst wenn x komplett falsch ist!

Lektion: Aus Appendix A - schwache Assertions sind ein häufiges Anti-Pattern in LLM-generierten Tests.

7.3 Limitation 3: Äquivalenzklassen falsch definiert

Szenario: Sie erreichen 100% EC Coverage, aber Sie haben eine wichtige Äquivalenzklasse verpasst.

Beispiel: Ihre EC-Partition enthält:

Aber Sie haben vergessen:

Was passiert?

Lektion: Coverage ist nur so gut wie Ihr Modell. Wenn Ihr EC-Modell unvollständig ist, ist 100% Coverage bedeutungslos.

7.4 Limitation 4: Coverage erkennt keine Mutation

Betrachte diesen Code:

if diff1 * diff2 <= 0:  # Original
    # finde Schnittpunkt

Eine Mutation (Bug durch Änderung des Operators eingeführt):

if diff1 * diff2 < 0:  # Mutiert (geändert <= zu <)
    # finde Schnittpunkt

Auswirkung: Der Fall, wo diff1 * diff2 == 0 (Strahl berührt Segment genau) wird jetzt verpasst.

Ihre Tests:

def test_intersection_found():
    # ... Test wo diff1 * diff2 = -5.0 (eindeutig < 0)
    assert x is not None  # ✅ Besteht

Coverage: 100% C1 ✅ (sowohl True als auch False Branches durch andere Tests abgedeckt)

Bug-Erkennung: 0% - der Test prüft nicht speziell die == 0 Grenze!

Lektion: Coverage sagt Ihnen, welche Zeilen ausgeführt wurden, nicht ob Ihre Tests Bugs fangen würden. Hier hilft Mutation Testing (zukünftiges Thema).


8. Moment… Wogegen testen wir eigentlich?

Lassen Sie uns pausieren und reflektieren, was wir in diesem Kurs bisher gemacht haben.

Was Sie gelernt hast:

Was wir nie richtig adressiert haben:

Wenn Sie einen Test wie diesen schreibst:

def test_finds_intersection_for_normal_angle() -> None:
    """Test dass find_intersection() einen gültigen Schnittpunkt zurückgibt..."""
    x, y, dist = find_intersection(x_road, y_road, angle=10.0, camera_x=0.0, camera_y=10.0)
    assert x is not None  # ← Aber... was macht dies "korrekt"?
    assert dist > 0       # ← Wer sagt das?

Die unbequeme Frage: Woher wissen Sie, was die Funktion tun sollte?

8.1 Die impliziten Anforderungen, die wir verwendet haben

Bis jetzt waren unsere “Anforderungen” an drei Orten versteckt:

1. Funktionssignaturen:

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

Implizite Anforderung: “Gibt drei Werte zurück: x, y, Distanz. Jeder kann None sein.”

2. Docstrings:

"""
Finde den Schnittpunkt zwischen Kamerastrahl und Straßenprofil.

Returns:
    tuple of (float, float, float) or (None, None, None)
        x, y Koordinaten des Schnittpunkts und Distanz von Kamera,
        oder None wenn kein Schnittpunkt
"""

Implizite Anforderung: “Gibt None zurück, wenn kein Schnittpunkt gefunden wird.”

3. Code-Kommentare:

# Überspringe, wenn dieses Segment hinter der Kamera ist
if x2 <= camera_x:
    continue

Implizite Anforderung: “Segmente hinter der Kamera sollten ignoriert werden.”

Das Problem: Diese impliziten Anforderungen sind unvollständig, mehrdeutig und verstreut.

Sie haben Code getestet, ohne zu wissen, was er tun soll!

8.2 Wie Coverage-Lücken fehlende Anforderungen aufdecken

Schauen wir uns an, was passiert, wenn wir versuchen, Coverage auf find_intersection() zu verbessern.

Ausgangspunkt: Der anfängliche Test

def test_finds_intersection_for_normal_angle():
    # Einfacher Fall: nach unten Strahl, nach oben Straße
    x, y, dist = find_intersection(
        x_road=np.array([0, 10, 20, 30], dtype=np.float64),
        y_road=np.array([0, 2, 4, 6], dtype=np.float64),
        angle_degrees=10.0,
        camera_x=0.0,
        camera_y=10.0
    )
    assert x is not None

Coverage: 60% C1 (Sie überprüfen den Coverage-Report)

Entdeckung 1: Nicht abgedeckte Zeile 98

Coverage-Report zeigt, dass Zeile 98 nicht abgedeckt ist:

if np.abs(np.cos(angle_rad)) < 1e-10:
    return None, None, None  # ← Zeile 98 nicht abgedeckt

Ihre Frage: “Moment, welche Bedingung löst das aus? Lass mich den Code lesen…”

(Sie analysieren: cos(angle_rad) ≈ 0 passiert, wenn angle_rad ≈ 90° oder 270° - vertikale Strahlen)

Ihre nächste Frage: “Okay, aber was sollte für vertikale Strahlen passieren? Ist None zurückgeben korrektes Verhalten?”

Das Problem: Es gibt keine Spezifikation, die sagt, was für vertikale Strahlen zu tun ist!

Sie könnten vernünftigerweise argumentieren:

Diese Coverage-Lücke enthüllt eine fehlende Anforderung.

Entdeckung 2: Nicht abgedeckte Zeile 109

Sie schreiben einen Test für vertikale Strahlen und überprüfst Coverage erneut. Jetzt wird Zeile 109 aufgedeckt:

if x2 <= camera_x:
    continue  # ← Zeile 109 nicht abgedeckt

Ihre Frage: “Wann wäre x2 <= camera_x? Oh, wenn Straßensegmente hinter der Kamera sind.”

Ihre nächste Frage: “Sollten Segmente hinter der Kamera ignoriert werden? Oder sollte es einen Fehler werfen?”

Wieder, es gibt keine Spezifikation!

Entdeckung 3: Nicht abgedeckte Zeile 123

Coverage-Report zeigt eine weitere Lücke:

if abs(diff2 - diff1) < 1e-10:
    # Parallele Linien
    t = 0  # ← Zeile 123 nicht abgedeckt

Ihre Frage: “Wann sind Strahl und Straßensegment parallel? Und warum setzt es t = 0? Ist das korrekt?”

An diesem Punkt realisieren Sie:

“Ich kann keine guten Tests schreiben, weil ich nicht wirklich weiß, was diese Funktion in all diesen Grenzfällen tun soll. Ich brauche eine systematische Spezifikation des Verhaltens!”

8.3 Von implizit zu explizit: Der Bedarf an Anforderungen

Was Sie gerade erlebt hast, ist das natürliche Entstehen von Requirements Engineering aus der Testpraxis.

Die Testing → Requirements Spirale:

1. Schreibe anfängliche Tests (basierend auf implizitem Verständnis)
      ↓
2. Messe Coverage (C1, EC)
      ↓
3. Finde nicht abgedeckte Code-Pfade
      ↓
4. Frage: "Was sollte hier passieren?"
      ↓
5. Realisiere: Die Spezifikation ist unklar/fehlt
      ↓
6. Schreibe explizite Anforderung auf
      ↓
7. Entwirf Test für diese Anforderung
      ↓
8. Wiederhole ab Schritt 2

Beispiel: Explizite Anforderungen entstehen aus Coverage-Analyse

Nach drei Iterationen der Coverage-Analyse haben Sie entdeckt, dass diese Anforderungen explizit sein müssen:

Szenario Implizit (Vorher) Explizit (Nachher)
Vertikale Strahlen (undefiniert - Code prüfen zum Raten) REQ-1: Für Winkel wo \(\lvert\cos(\theta)\rvert < 10^{-10}\), gebe (None, None, None) zurück
Segmente hinter Kamera (nur Code-Kommentar) REQ-2: Betrachte nur Straßensegmente wo x > camera_x. Segmente an oder hinter der Kamera sollen ignoriert werden.
Paralleler Strahl/Segment (unklare Magic Number) REQ-3: Wenn Strahl und Straßensegment parallel sind (\(\lvert\Delta_{diff}\rvert < 10^{-10}\)), verwende ersten Endpunkt als Schnittpunkt.
Kein Schnittpunkt gefunden (Docstring erwähnt es) REQ-4: Wenn kein Straßensegment den Strahl schneidet, gebe (None, None, None) zurück
Mehrere Schnittpunkte (komplett unspezifiziert!) REQ-5: Wenn mehrere Straßensegmente den Strahl schneiden, gebe den ersten Schnittpunkt zurück (kleinste Distanz von Kamera).

Beachte, was passiert ist:

8.4 Tests als ausführbare Spezifikationen

Hier ist eine mächtige Erkenntnis:

Ihre Tests SIND Anforderungen, nur in ausführbarer Form geschrieben.

Vergleiche:

Traditionelles Anforderungsdokument:

“REQ-1: Das System soll None für vertikale Kamerastrahlen zurückgeben.”

Ausführbare Spezifikation (pytest Test):

def test_vertical_ray_returns_none():
    """REQ-1: Vertikale Strahlen sollen None zurückgeben."""
    x, y, dist = find_intersection(
        x_road=np.array([0, 10], dtype=np.float64),
        y_road=np.array([0, 5], dtype=np.float64),
        angle_degrees=90.0,  # Vertikaler Strahl
        camera_x=0.0,
        camera_y=10.0
    )
    assert x is None
    assert y is None
    assert dist is None

Welches ist besser?

Traditionelle Anforderung Test als Spezifikation
Kann mehrdeutig sein Eindeutig (führt aus!)
Kann veraltet werden Schlägt fehl, wenn Code nicht passt
Passiv (jemand muss es lesen) Aktiv (läuft bei jedem Commit)
Getrennt vom Code Lebt mit dem Code

Das ist die Brücke zwischen Testing und Requirements Engineering:

8.5 Wie geht es weiter?

Sie haben jetzt eine wichtige Einsicht erreicht:

Testing und Requirements Engineering sind zwei Seiten derselben Medaille.

Was Sie bisher gelernt hast:

Was als nächstes kommt (zukünftige Vorlesungen):

Für jetzt, merken Sie sich dies:

Wenn Sie eine Coverage-Lücke siehst, füge nicht einfach einen Test hinzu, um den Prozentsatz zu erhöhen. Frage, welche Anforderung die Lücke enthüllt. Schreibe diese Anforderung explizit auf. Dann schreibe den Test.

Die Spirale geht weiter:

Coverage-Lücke → Anforderungsfrage → Explizite Spezifikation → Test-Design → Coverage-Verbesserung
     ↑                                                                              ↓
     └──────────────────────────────────────────────────────────────────────────────┘

Dieser iterative Prozess ist kein Bug—er ist ein Feature. So entdecken echte Softwareprojekte, was sie eigentlich tun sollen.


9. Zusammenfassung

Lassen Sie uns konsolidieren, was Sie gelernt haben.

Konzept Definition Wichtige Einsicht Praktische Schlussfolgerung
Programm P Das zu testende Software-Artefakt Testing ist immer über ein spezifisches Programm Sei klar über Umfang (Funktion? Modul? System?)
Eingabebereich D Menge aller möglichen Eingaben für P Normalerweise unendlich - Sie können nicht alles testen Brauche systematische Strategien zur Stichprobenziehung aus D
Testsuite T Endliche Teilmenge von D, die Sie tatsächlich testest T ⊆ D, und |T| << |D| Qualität von T wichtiger als Größe
Modell M(P) Abstraktion von P's Struktur oder Verhalten Coverage ist immer Coverage eines Modells Verschiedene Modelle → verschiedene Testanforderungen
Coverage-Kriterium C Regel, die Testanforderungen R_C(P) definiert C definiert, was "adäquates" Testing bedeutet Wähle C basierend auf Risiko und Kosten
Statement Coverage (C0) Führe jede Anweisung mindestens einmal aus Schwächstes strukturelles Kriterium Baseline - ziele höher als C0
Branch Coverage (C1) Übe jedes Entscheidungsergebnis aus (T/F) C1 subsumiert C0 Gutes Ziel für Unit-Tests (70-80%+ C1)
Äquivalenzklassen Partition des Eingabebereichs EC ist ein Coverage-Kriterium über Eingabe-Modell Verwende ECs + C1 zusammen (komplementär)
Grenzwerte Werte an Kanten von Äquivalenzklassen Bugs häufen sich an Grenzen Teste Grenzen immer explizit
Coverage-Limitationen 100% Coverage ≠ fehlerfrei Coverage misst Ausführung, nicht Verifikation Brauche starke Assertions + gutes Modell
Requirements ↔ Tests Spirale Iterativer Verfeinerungsprozess Testing entdeckt Anforderungslücken Nutze Coverage zur Anforderungsermittlung

10. Reflexionsfragen

Bevor Sie weitergehen, nehmen Sie sich ein paar Minuten Zeit, über diese Fragen nachzudenken. Schreiben Sie Ihre Antworten auf - das wird Ihr Verständnis vertiefen.

Frage 1: Modelle und Coverage

Was ist das Modell hinter Statement Coverage (C0)? Was ist das Modell hinter Equivalence Class Coverage?

Wie unterscheiden sich diese Modelle, und warum ist das für Testing wichtig?

Frage 2: Coverage Stakeholdern erklären

Stellen Sie sich vor, ein Projektmanager sagt: “Großartig, wir haben 100% Branch Coverage! Das bedeutet keine Bugs, richtig?”

Wie würden Sie erklären, warum das nicht stimmt? Gib ein spezifisches Beispiel mit geometry.py.

Frage 3: Neue Äquivalenzklassen entwerfen

Schauen Sie sich die Funktion calculate_ray_line() aus geometry.py an (hier nicht im Detail gezeigt, aber sie berechnet Strahlkoordinaten gegeben einen Winkel).

Entwirf drei Äquivalenzklassen für den angle_degrees Parameter. Für jede Klasse:

Frage 4: Coverage-getriebene Anforderungen

Sie führen Coverage-Analyse auf find_intersection() aus und entdeckst, dass die Zeile t = 0 (im parallelen Linien Branch) nie ausgeführt wird.

Welche Frage sollten Sie über die Anforderungen stellen? Welchen Test würden Sie schreiben, nachdem Sie die Anforderung geklärt haben?

Frage 5: Jenseits von Unit-Testing

Diese Vorlesung fokussierte auf Unit-Testing-Coverage. Wie würden Sie die Konzepte von Programm, Modell, Testsuite und Coverage-Kriterium anwenden auf:

Was wäre anders, und was bliebe gleich?


11. Weiterführende Literatur

Wenn Sie tiefer in Testtheorie und Coverage eintauchen möchtest:

Bücher:

Papers:

Tools:

Standards:

Nächste Themen (Zukünftige Vorlesungen oder Projekte):


12. Appendix: Lösungsskizze für Übungen

Hier ist eine mögliche Menge von Tests, die höhere Coverage für find_intersection() erreichen. Dies sind nicht die einzigen Lösungen - es gibt viele Wege, Coverage zu erreichen!

12.1 Übung 1 Lösung: Null Schleifeniterationen

def test_empty_road_array_returns_none():
    """
    Test dass find_intersection() None für ein leeres Straßenarray zurückgibt.

    Branch Coverage: B5 False-Pfad (null Schleifeniterationen)
    Äquivalenzklasse: EC8 (leere Straße)
    """
    # Arrange: Leere Arrays
    x_road = np.array([], dtype=np.float64)
    y_road = np.array([], dtype=np.float64)

    # Act
    x, y, dist = find_intersection(x_road, y_road, 45.0, 0.0, 5.0)

    # Assert
    assert x is None
    assert y is None
    assert dist is None

12.2 Übung 2 Lösung: Strahl kreuzt Segment nicht

def test_ray_misses_road_entirely():
    """
    Test dass find_intersection() None zurückgibt, wenn Strahl alle Straßensegmente verfehlt.

    Branch Coverage: B3 False-Pfad (mehrmals), B6 True-Pfad
    Äquivalenzklasse: EC2 (kein Schnittpunkt)
    """
    # Arrange: Straße unten, Strahl zeigt nach oben
    x_road = np.array([0, 10, 20], dtype=np.float64)
    y_road = np.array([0, 1, 2], dtype=np.float64)  # Straße bei niedrigen y-Werten

    # Nach oben Strahl (negativer Winkel = nach oben von horizontal)
    angle = -45.0
    camera_x = 5.0
    camera_y = 10.0  # Kamera hoch über Straße

    # Act
    x, y, dist = find_intersection(x_road, y_road, angle, camera_x, camera_y)

    # Assert: Strahl zeigt nach oben, Straße ist unten - kein Schnittpunkt
    assert x is None
    assert y is None
    assert dist is None

12.3 Übung 3 Lösung: Schnittpunkt am zweiten Segment

def test_intersection_on_second_segment():
    """
    Test dass find_intersection() Schnittpunkt am zweiten Straßensegment findet,
    nicht am ersten (erstes Segment sollte verfehlen).

    Branch Coverage: B3 False (erstes Segment), B3 True (zweites Segment)
    Äquivalenzklasse: EC5 (mehrere Segmente, selektiver Schnittpunkt)
    """
    # Arrange: Zwei Segmente, Strahl trifft nur das zweite
    # Erstes Segment: (0,0) zu (10,0) - horizontal bei y=0
    # Zweites Segment: (10,0) zu (20,10) - nach oben Steigung
    x_road = np.array([0, 10, 20], dtype=np.float64)
    y_road = np.array([0, 0, 10], dtype=np.float64)

    # Strahl von Kamera bei (15, 20) nach unten bei 45°
    angle = 45.0
    camera_x = 15.0
    camera_y = 20.0

    # Act
    x, y, dist = find_intersection(x_road, y_road, angle, camera_x, camera_y)

    # Assert: Sollte Schnittpunkt am zweiten Segment finden
    assert x is not None
    assert y is not None
    assert 10 < x < 20, "Schnittpunkt sollte am zweiten Segment sein (x zwischen 10 und 20)"
    assert 0 < y < 10, "Schnittpunkt sollte am zweiten Segment sein (y zwischen 0 und 10)"
    assert dist > 0

12.4 Übung 4 Lösung: Schnittpunkt am Straßeneckpunkt

def test_intersection_at_road_vertex():
    """
    Test Schnittpunkt, wenn Strahl genau durch einen Straßeneckpunkt geht.

    Branch Coverage: B3 True (mit diff1 oder diff2 genau 0)
    Äquivalenzklasse: EC6 (Schnittpunkt am Eckpunkt)
    Grenzfall: diff1 * diff2 = 0 (Strahl genau am Eckpunkt)
    """
    # Arrange: Straße mit Eckpunkt bei (10, 5)
    x_road = np.array([0, 10, 20], dtype=np.float64)
    y_road = np.array([0, 5, 10], dtype=np.float64)

    # Berechne Winkel sodass Strahl durch (10, 5) geht
    # Kamera bei (0, 10), Ziel (10, 5)
    # Steigung = (5 - 10) / (10 - 0) = -5/10 = -0.5
    # angle = arctan(-0.5) ≈ -26.57°
    # Aber erinnere: Winkel wird nach unten gemessen, und wir negieren ihn im Code
    # Also wir wollen Winkel sodass tan(-angle_rad) = -0.5
    # Das bedeutet angle ≈ 26.57° (nach unten)

    camera_x = 0.0
    camera_y = 10.0
    angle = np.rad2deg(np.arctan(0.5))  # ≈ 26.57°

    # Act
    x, y, dist = find_intersection(x_road, y_road, angle, camera_x, camera_y)

    # Assert: Sollte Schnittpunkt am Eckpunkt (10, 5) finden
    assert x is not None
    assert y is not None
    # Verwende pytest.approx für Fließkomma-Vergleich
    from pytest import approx
    assert x == approx(10.0, abs=0.01), "Sollte bei x=10 schneiden"
    assert y == approx(5.0, abs=0.01), "Sollte bei y=5 schneiden"
    assert dist == approx(np.sqrt(100 + 25), abs=0.01), "Distanz sollte sqrt(125) sein"

Hinweis: Dies sind Beispiellösungen. Sie könnten andere Tests entwerfen, die dieselbe Coverage erreichen. Der Schlüssel ist:

  1. Verstehen, welchen Branch/EC Sie anvisieren
  2. Eingaben entwerfen, die dieses spezifische Verhalten auslösen
  3. Starke Assertions schreiben, die Korrektheit verifizieren (nicht nur “is not None”)
© 2026 Dominik Mueller   •  Powered by Soopr   •  Theme  Moonwalk