03 Grundlagen des Testens: Testtheorie, Coverage und Anforderungen
December 2025 (9446 Words, 53 Minutes)
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:
- Was Coverage wirklich misst
- Warum es verschiedene Arten von Coverage gibt (Statement, Branch, Path)
- Wie man systematisch Tests entwirft, um bestimmte Coverage-Ziele zu erreichen
- Wann Coverage “ausreichend” ist vs. wann man mehr Tests braucht
- Wie Coverage mit Anforderungen zusammenhängt
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:
- Das theoretische Framework verstehen für Testing: Programm, Eingabebereich, Testsuite, Modell und Coverage-Kriterium
- Verschiedene Arten von Code-Coverage erklären: Statement (C0) und Branch (C1) Coverage
- Systematisch Tests entwerfen, um bestimmte Coverage-Ziele zu erreichen (z.B. 100% Branch Coverage)
- Erkennen, dass Äquivalenzklassen und Grenzwerte auch Coverage-Kriterien über ein Eingabebereichs-Modell sind
- Die Grenzen von Coverage-Metriken verstehen
- Eine iterative Spirale zwischen Anforderungen, Tests und Coverage anwenden, um beide zu verfeinern
Was Sie LERNEN wirst:
- Wie man einen Coverage-Report strategisch liest
- Wie man identifiziert, welche Branches/Statements Tests benötigen
- Wie man systematisch Tests mit Control Flow Graphs entwirft
- Warum 100% Coverage nicht fehlerfreie Software bedeutet
Was Sie NICHT lernen wirst:
- Erweiterte Coverage-Typen (Path Coverage, MC/DC) - das kommt in späteren Kursen
- Coverage für Integrations-/E2E-Tests - wir fokussieren auf Unit-Tests
- Mutation Testing - ein ganz anderes Thema
- Wie man Coverage-Metriken austrickst (bitte nicht!)
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.
- Einfaches Beispiel: \(P = \text{classify_number}()\)
- Reales Beispiel: \(P = \text{find_intersection}()\) aus
geometry.py
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:
- \(EC_1 = \{x \in \mathbb{R} : x > 0\}\) → Repräsentant: \(5.0\)
- \(EC_2 = \{x \in \mathbb{R} : x < 0\}\) → Repräsentant: \(-3.14\)
- \(EC_3 = \{x \in \mathbb{R} : x = 0\}\) → Repräsentant: \(0.0\)
Ä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:
- Control Flow Graph (CFG): \(M(P)\) ist ein gerichteter Graph \(G = (V, E)\)
- Call Graph: \(M(P)\) ist ein gerichteter Graph \(G = (V, E)\)
- Data Flow Graph: \(M(P)\) ist eine Menge von Definition-Use-Paaren
Semantische Modelle (Black-Box)
Diese Modelle werden vom beabsichtigten Verhalten des Programms abgeleitet:
- Input Domain Partition: \(M(P)\) ist eine Partition von \(D\) in Äquivalenzklassen
- State Machine: \(M(P)\) ist ein endlicher Automat \((Q, \Sigma, \delta, q_0, F)\)
- Decision Tables: \(M(P)\) ist eine Tabelle, die Bedingungen auf Aktionen abbildet
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:
- \(x > 0\) → return “positive” (führt Kanten aus: Start→Check1, Check1→Positive, Positive→End)
- \(x \leq 0\) und \(x < 0\) → return “negative” (führt Kanten aus: Start→Check1, Check1→Check2, Check2→Negative, Negative→End)
- \(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:
- \(M(P)\) ein konkretes mathematisches Objekt ist (wie ein Graph)
- \(R_C(P)\) von Elementen von \(M(P)\) abgeleitet wird (wie Kanten oder Knoten)
- Verschiedene \(M(P)\) Typen zu verschiedenen \(R_C(P)\) Mengen führen
…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:
- Mehrere Branches (mindestens 4 wichtige Entscheidungspunkte: vertikaler Strahl, hinter Kamera, Strahl kreuzt, parallele Linien)
- Eine Schleife (erzeugt viele mögliche Pfade - jede Iteration kann verschiedene Branches nehmen)
- Frühe Returns (3 verschiedene Ausstiegspunkte: vertikaler Strahl → None, Schnittpunkt gefunden → Koordinaten, kein Schnittpunkt → None)
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:
- Definition von
yin Zeile 2 → Verwendung vonyin Zeile 3 - Definition von
zin Zeile 3 → Verwendung vonzin Zeile 4
Zusammenfassung: Warum Modelle wichtig sind
Verschiedene Modelle führen zu verschiedenen Coverage-Kriterien:
- CFG-Modell → Statement Coverage, Branch Coverage, Path Coverage
- Input Domain Partition-Modell → Equivalence Class Coverage (Kapitel 03 (Grundlagen des Testens)!)
- Call Graph-Modell → Function Coverage
- Data Flow-Modell → Definition-Use Coverage
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:
- Führt eine bestimmte Anweisung aus
- Traversiert eine bestimmte Kante
- Testet eine spezifische Äquivalenzklasse
- Deckt ein bestimmtes Definition-Use-Paar ab
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:
- \(\vert T \vert = 1\) (ein Test)
- \(\vert R_{C_{stmt}}(\text{find_intersection})\vert = 22\) (22 Anweisungen)
- \(\vert\text{cov}_{C_{stmt}}(T, \text{find_intersection})\vert \approx 13\) (nur 13 Anweisungen ausgeführt)
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.
- ✅ Statement Coverage: endlich (\(\vert R_{C_{stmt}}(P)\vert\) = Anzahl der Anweisungen)
- ✅ Branch Coverage: endlich (\(\vert R_{C_{branch}}(P)\vert\) = Anzahl der Branches)
- ❌ Path Coverage: unendlich für Programme mit Schleifen (unmöglich 100% zu 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:
- \(T_1 = \lbrace\text{test}_1\rbrace\) deckt Anweisungen \(\lbrace 1, 2, 5, 10\rbrace\) ab
- \(T_2 = \lbrace\text{test}_1, \text{test}_2\rbrace\) deckt Anweisungen \(\lbrace 1, 2, 5, 10, 15, 18\rbrace\) ab
- \(\text{cov}_{C_{stmt}}(T_1, P) \subseteq \text{cov}_{C_{stmt}}(T_2, P)\) ✅
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:
- Minimale adäquate Suiten: Adäquate Testsuiten, für die keine echte Teilmenge adäquat ist
- Kosten: Größe von \(R_C(P)\), Komplexität der Coverage-Berechnung
- Fehlererkennungsfähigkeit: Hilft das Erfüllen von \(C\) tatsächlich, Bugs zu finden?
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:
- \(C_1\) ist mindestens so stark wie \(C_2\)
- 100% \(C_1\) zu erreichen gibt Ihnen automatisch 100% \(C_2\)
- \(C_1\) hat mehr Anforderungen (oder äquivalente Anforderungen)
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:
- \(V\) = Menge der Knoten (Anweisungen)
- \(E\) = Menge der Kanten (Branches)
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:
- Einen Quellknoten \(v_1\)
- Einen Zielknoten \(v_2\)
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:
- Sie führen sowohl True als auch False Branches von
if np.abs(np.cos(angle_rad)) < 1e-10aus - Das bedeutet, Sie führen aus:
- Die
ifAnweisung selbst ✅ - Die
return None, None, NoneAnweisung (True Branch) ✅ - Die
slope = np.tan(angle_rad)Anweisung (False Branch) ✅
- Die
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:
- Vertikaler Strahl (True Branch des ersten
if) - Straße hinter Kamera (True Branch des zweiten
if) - Parallele Linien (True Branch des vierten
if)
Also: Hohe C0 ≠ Hohe C1, aber Hohe C1 → Hohe C0.
Warum Subsumption wichtig ist:
- Leitet Kriterienauswahl: Wähle stärkere Kriterien, wenn Sie sie sich leisten können
- Vermeide redundante Arbeit: Messe nicht sowohl C1 als auch C0 (C1 ist ausreichend)
- Theoretische Grundlagen: Hilft, verschiedene Teststrategien formal zu vergleichen
Weitere Subsumptionsbeziehungen (zukünftige Themen):
- Modified Condition/Decision Coverage (MC/DC) subsumiert Branch Coverage
- All-Paths Coverage subsumiert MC/DC
- Mutation Score hat komplexe Beziehungen zu struktureller Coverage
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:
- Branch Coverage: Stellt sicher, dass Sie alle Code-Pfade ausüben
- EC Coverage: Stellt sicher, dass Sie alle semantischen Verhaltensweisen testen
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:
condition = True(führtdo_something()aus)condition = False(überspringtdo_something())
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:
- True Branch führt
do_something()aus ✅ - False Branch überspringt es ✅
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:
if np.abs(np.cos(angle_rad)) < 1e-10(vertikaler Strahl Check)for i in range(len(x_road) - 1)(Schleife - 0 vs 1+ Iterationen)if x2 <= camera_x(hinter Kamera Check)if diff1 * diff2 <= 0(Schnittpunkt Check)if abs(diff2 - diff1) < 1e-10(parallel Check)- 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:
- Welche Eingabe macht diese Bedingung True?
- Welche Eingabe macht diese Bedingung False?
Schritt 3: Schleifenabdeckung prüfen
- 0 Iterationen: Welche Eingabe bewirkt, dass die Schleife nie ausgeführt wird?
- 1 Iteration: Welche Eingabe bewirkt genau eine Iteration?
- Viele Iterationen: Welche Eingabe bewirkt mehrere Iterationen?
Schritt 4: Alle Return-Pfade prüfen
- Frühe Returns (von
ifAnweisungen) - Normales Return (nach Schleife)
- Exception-Pfade (falls vorhanden)
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:
- \(\bigcup_{i=1}^{n} EC_i = D\) (Klassen decken alle Eingaben ab)
- \(EC_i \cap EC_j = \emptyset\) für \(i \neq j\) (Klassen sind disjunkt)
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!
- Branch Coverage (C1) stellt sicher, dass Sie alle Code-Pfade ausüben (strukturelle Coverage)
- EC/BVA Coverage stellt sicher, dass Sie alle semantischen Verhaltensweisen testen (funktionale Coverage)
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?
- Branch Coverage: ✅ (Zeile ausgeführt)
- Test-Assertion: ✅ (dist ist nicht None)
- Korrektheit: ❌ (dist ist falsch berechnet!)
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:
- EC1: Einzelner Schnittpunkt ✅
- EC2: Kein Schnittpunkt ✅
- EC3: Vertikaler Strahl ✅
Aber Sie haben vergessen:
- EC_missing: Schnittpunkt genau an einem Segment-Endpunkt (Grenzfall)
Was passiert?
- Coverage-Report: 100% EC Coverage ✅
- Bug: Grenzfall am Segment-Endpunkt schlägt fehl ❌
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:
- Wie man Tests schreibt (Kapitel 01 (Moderne Entwicklungswerkzeuge): AAA-Pattern, pytest)
- Wie man Tests entwirft (Kapitel 03 (Grundlagen des Testens): Äquivalenzklassen, Grenzwerte)
- Wie man Testqualität misst (diese Vorlesung: Coverage-Kriterien)
- Wie man Coverage-Lücken interpretiert und Testsuiten verbessert
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.
- Was passiert, wenn der Strahl vertikal ist?
- Was, wenn
x_roadundy_roadunterschiedliche Längen haben? - Was, wenn die Arrays leer sind?
- Was, wenn der Winkel > 360° ist?
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:
- ❓ None zurückgeben (aktuelle Implementierung)
- ❓ Eine Exception werfen (
ValueError("Vertikale Strahlen nicht unterstützt")) - ❓ Vertikale Strahlen speziell behandeln (anderer Algorithmus)
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:
- Coverage-Lücken → Spezifische Fragen → Explizite Anforderungen
- Testing zwang Sie, die Spezifikation zu klären
- Anforderungen sind jetzt testbar (jedes REQ-N bildet auf Testfälle ab)
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:
- Gute Anforderungen leiten Test-Design
- Gute Tests validieren Anforderungen
- Coverage-Metriken enthüllen unvollständige Anforderungen
- Der Prozess ist iterativ und bidirektional
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:
- Wie man Tests schreibt (Implementierungsperspektive)
- Wie man Testqualität misst (Coverage-Perspektive)
- Wie Tests fehlende Spezifikationen aufdecken (Entdeckungsperspektive)
Was als nächstes kommt (zukünftige Vorlesungen):
- Wie man systematisch Anforderungen ermittelt (Requirements Engineering)
- Wie man formale Spezifikationen schreibt (Design by Contract, Assertions)
- Wie man Tests aus Anforderungen generiert (Model-Based Testing)
- Wie man sicherstellt, dass Anforderungen testbar sind (Testability Analysis)
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:
- Definiere den Bereich
- Beschreibe das erwartete Verhalten
- Gib einen repräsentativen Testwert
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:
- Integrationstests?
- End-to-End-Tests?
- UI-Tests?
Was wäre anders, und was bliebe gleich?
11. Weiterführende Literatur
Wenn Sie tiefer in Testtheorie und Coverage eintauchen möchtest:
Bücher:
- Ammann & Offutt: Introduction to Software Testing (2nd ed.) - Das definitive Lehrbuch zur Testtheorie
- Myers, Sandler & Badgett: The Art of Software Testing (3rd ed.) - Klassiker, sehr lesbar
- Pezze & Young: Software Testing and Analysis - Theoretischer, großartig zum Verständnis von Modellen
Papers:
- Zhu, Hall & May (1997): “Software Unit Test Coverage and Adequacy” - Überblick über Coverage-Kriterien
- Inozemtseva & Holmes (2014): “Coverage is Not Strongly Correlated with Test Suite Effectiveness” - Wichtige empirische Studie
Tools:
- pytest-cov Dokumentation: https://pytest-cov.readthedocs.io/
- Coverage.py Docs: https://coverage.readthedocs.io/ (das zugrunde liegende Tool)
- Hypothesis: https://hypothesis.readthedocs.io/ (Property-Based Testing - generiert Tests automatisch!)
Standards:
- IEEE 829: Standard für Software-Testdokumentation
- ISO/IEC/IEEE 29119: Software Testing Standards (neuer, umfassender)
Nächste Themen (Zukünftige Vorlesungen oder Projekte):
- Path Coverage - Gründlicher als Branch Coverage, aber exponentiell schwieriger
- Modified Condition/Decision Coverage (MC/DC) - Erforderlich für sicherheitskritische Systeme (Luftfahrt, Medizingeräte)
- Mutation Testing - Wie man seine Tests testet
- Property-Based Testing - Tests automatisch aus Eigenschaften generieren
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:
- Verstehen, welchen Branch/EC Sie anvisieren
- Eingaben entwerfen, die dieses spezifische Verhalten auslösen
- Starke Assertions schreiben, die Korrektheit verifizieren (nicht nur “is not None”)