Home

03 Grundlagen des Testens: Automatisiertes Testen in CI & Test-Coverage

lecture ci-cd github-actions pytest test-coverage coverage-py tdd automation

1. Einführung: Ihre CI prüft den Stil, aber prüft sie auch die Korrektheit?

In Kapitel 02 (Feature-Entwicklung) haben Sie gelernt, wie man GitHub Actions CI/CD einrichtet, um Code-Qualität automatisch zu überprüfen:

In Kapitel 03 (Grundlagen des Testens) haben Sie gelernt, wie man Tests mit pytest schreibt:

Aber hier ist das Problem:

# Sie schreiben großartige Tests
$ pytest tests/
================================ test session starts =================================
collected 47 items

tests/test_geometry.py ......                                                  [ 12%]
tests/test_road.py .........                                                   [ 31%]
tests/test_visualization.py ....                                               [ 40%]
tests/test_integration.py ..........                                           [ 61%]
tests/test_edge_cases.py ..................                                    [100%]

================================ 47 passed in 2.34s ==================================

# Sie pushen zur CI...
$ git push origin feature/add-input-validation

# GitHub Actions läuft...
✅ Ruff linter - PASSED
✅ Ruff format - PASSED
✅ Pyright - PASSED

# Aber Ihre Tests? Wurden nie ausgeführt! 😱

Die Lücke:

Was Sie in dieser Vorlesung lernen werden:

  1. CI + Tests: Wie man pytest zu GitHub Actions hinzufügt, um Tests obligatorisch zu machen
  2. Test-Coverage: Wie man messbar macht, welcher Code getestet ist (und welcher nicht)
  3. Test-Driven Development (TDD): Eine optionale Disziplin, die beim Schreiben von besserem Code hilft

Pädagogischer Hinweis: Erst Praxis, dann Theorie

In diesem Kurs folgen wir einem “Praxis zuerst”-Ansatz für das Testen:

Dies spiegelt wider, wie professionelle Entwickler lernen: erst die Dinge zum Laufen bringen, dann die Theorie dahinter verstehen. Wenn Sie Kapitel 03 (Testtheorie und Coverage) erreichen, haben Sie bereits praktische Erfahrung mit Coverage-Metriken, was die Theorie bedeutungsvoller macht.

Was Sie in der NÄCHSTEN Vorlesung (Kapitel 03 (Testtheorie und Coverage)) lernen werden:


2. Teil 1: Hinzufügen von pytest zu GitHub Actions CI

2.1. Warum Tests in CI laufen müssen

Szenario: Sie sind Teil eines Teams mit 5 Entwicklern. Jeder arbeitet an Feature-Branches:

# Entwickler A
$ git checkout -b feature/add-angle-validation
# ... ändert geometry.py ...
$ pytest tests/  # ✅ Alle Tests bestehen lokal
$ git push origin feature/add-angle-validation

# Entwickler B (gleichzeitig)
$ git checkout -b feature/optimize-ray-calculation
# ... ändert geometry.py (dieselbe Datei!) ...
$ pytest tests/  # ✅ Alle Tests bestehen lokal
$ git push origin feature/optimize-ray-calculation

Was kann schiefgehen?

  1. Entwickler A’s PR wird gemergedmain Branch wird aktualisiert
  2. Entwickler B’s Branch ist jetzt veraltet (basiert auf alter main)
  3. Entwickler B’s PR wird gemerged → Keine Konflikte (verschiedene Zeilen)
  4. Aber kombiniert brechen die Änderungen die Tests! 💥

Ohne CI-Tests:

Mit CI-Tests:

Ziel: Machen Sie Tests obligatorisch indem Sie sie in CI laufen lassen.


2.2. Ihre aktuelle CI-Pipeline (aus Kapitel 02)

Hier ist die .github/workflows/quality.yml die Sie in Kapitel 02 (Feature-Entwicklung) erstellt haben:

name: Code Quality Checks

on:
  pull_request:
    branches: [main]
  push:
    branches: [main]

jobs:
  quality:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.12'

      - name: Install uv
        uses: astral-sh/setup-uv@v5
        with:
          version: "latest"

      - name: Install dependencies
        run: uv sync

      - name: Run Ruff linter
        run: uv run ruff check code_examples/

      - name: Run Ruff formatter
        run: uv run ruff format --check code_examples/

      - name: Run Pyright
        run: uv run pyright code_examples/

Was diese Pipeline macht:

Was sie NICHT macht:


2.3. Hinzufügen von pytest zu Ihrer CI-Pipeline

Schritt 1: Fügen Sie einen pytest-Schritt zu Ihrem Workflow hinzu

name: Code Quality Checks

on:
  pull_request:
    branches: [main]
  push:
    branches: [main]

jobs:
  quality:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.12'

      - name: Install uv
        uses: astral-sh/setup-uv@v5
        with:
          version: "latest"

      - name: Install dependencies
        run: uv sync

      - name: Run Ruff linter
        run: uv run ruff check code_examples/

      - name: Run Ruff formatter
        run: uv run ruff format --check code_examples/

      - name: Run Pyright
        run: uv run pyright code_examples/

      # 🆕 NEU: Führe Tests aus
      - name: Run tests
        run: uv run pytest tests/ -v

Was ändert sich:

Was passiert jetzt:

  1. Jeder PR muss alle Tests bestehen, bevor er gemerged werden kann
  2. Wenn ein Test fehlschlägt → CI schlägt fehl → PR kann nicht gemerged werden
  3. main Branch bleibt immer funktionsfähig (alle Tests bestehen)

2.4. Testen der neuen Pipeline

Schritt 1: Erstelle einen Feature-Branch

$ git checkout -b feature/add-pytest-to-ci

Schritt 2: Aktualisiere .github/workflows/quality.yml

Füge den pytest-Schritt wie oben gezeigt hinzu.

Schritt 3: Commit und Push

$ git add .github/workflows/quality.yml
$ git commit -m "Add pytest to CI pipeline"
$ git push origin feature/add-pytest-to-ci

Schritt 4: Erstelle einen Pull Request

Auf GitHub:

  1. Gehen Sie zu Ihrem Repository
  2. Klicke “Pull requests” → “New pull request”
  3. Wähle feature/add-pytest-to-cimain
  4. Klicke “Create pull request”

Schritt 5: Beobachte GitHub Actions

Die CI wird automatisch laufen:

Code Quality Checks
├─ Checkout code          ✅ PASSED
├─ Set up Python          ✅ PASSED
├─ Install uv             ✅ PASSED
├─ Install dependencies   ✅ PASSED
├─ Run Ruff linter        ✅ PASSED
├─ Run Ruff formatter     ✅ PASSED
├─ Run Pyright            ✅ PASSED
└─ Run tests              ✅ PASSED (47 tests in 2.34s)

Wenn alle Checks grün sind:

Wenn ein Test fehlschlägt:


2.5. Branch Protection Rules: Tests obligatorisch machen

Problem: Auch mit Tests in CI könnte jemand die Checks umgehen und direkt zu main pushen.

Lösung: Branch Protection Rules erzwingen, dass alle CI-Checks bestehen müssen.

Schritt 1: Aktiviere Branch Protection auf GitHub

  1. Gehen Sie zu Ihrem Repository auf GitHub
  2. Klicken Sie SettingsBranches
  3. Klicken Sie Add rule unter “Branch protection rules”
  4. Branch name pattern: main
  5. Aktivieren Sie diese Einstellungen:
    • Require a pull request before merging
    • Require status checks to pass before merging
      • Suchen und fügen Sie hinzu: quality (der Job-Name aus Ihrem Workflow)
    • Require branches to be up to date before merging
    • Include administrators (gilt für jeden, auch Sie!)
  6. Klicken Sie Create

Was das macht:


2.6. Beispiel: Was passiert, wenn ein Test fehlschlägt

Szenario: Sie änderst geometry.py, aber brichst versehentlich einen Test.

# code_examples/geometry.py
def find_intersection(x_road, y_road, angle_degrees, camera_x=0, camera_y=1.5):
    """Find intersection of camera ray with road profile."""

    # 🐛 BUG: Vergessen zu validieren, dass Arrays nicht leer sind
    # if len(x_road) == 0 or len(y_road) == 0:
    #     return None

    angle_rad = -np.deg2rad(angle_degrees)
    slope = np.tan(angle_rad)
    intercept = camera_y - slope * camera_x

    # ... Rest der Funktion ...

Sie pushen die Änderung:

$ git add code_examples/geometry.py
$ git commit -m "Optimize intersection calculation"
$ git push origin feature/optimize-intersection

GitHub Actions führt Tests aus:

Run tests
============================= test session starts ==============================
collected 47 items

tests/test_geometry.py .....F                                            [ 12%]
tests/test_road.py .........                                             [ 31%]
tests/test_visualization.py ....                                         [ 40%]
tests/test_integration.py ..........                                     [ 61%]
tests/test_edge_cases.py ..................                              [100%]

=================================== FAILURES ===================================
___________ test_find_intersection_empty_arrays ___________

    def test_find_intersection_empty_arrays():
        """Test that empty arrays are handled gracefully."""
        x_road = np.array([])
        y_road = np.array([])

        result = find_intersection(x_road, y_road, angle_degrees=-5.0)

>       assert result is None, "Should return None for empty arrays"
E       IndexError: index 0 is out of bounds for axis 0 with size 0

tests/test_geometry.py:45: IndexError
=========================== 1 failed, 46 passed in 2.41s ===========================
Error: Process completed with exit code 1.

Resultat:

Der Fix:

# code_examples/geometry.py
def find_intersection(x_road, y_road, angle_degrees, camera_x=0, camera_y=1.5):
    """Find intersection of camera ray with road profile."""

    # ✅ FIX: Validiere leere Arrays
    if len(x_road) == 0 or len(y_road) == 0:
        return None

    angle_rad = -np.deg2rad(angle_degrees)
    slope = np.tan(angle_rad)
    intercept = camera_y - slope * camera_x

    # ... Rest der Funktion ...

Sie pushen den Fix:

$ git add code_examples/geometry.py
$ git commit -m "Fix: Handle empty arrays in find_intersection"
$ git push origin feature/optimize-intersection

GitHub Actions läuft erneut:

Run tests
============================= test session starts ==============================
collected 47 items

tests/test_geometry.py ......                                            [ 12%]
tests/test_road.py .........                                             [ 31%]
tests/test_visualization.py ....                                         [ 40%]
tests/test_integration.py ..........                                     [ 61%]
tests/test_edge_cases.py ..................                              [100%]

============================= 47 passed in 2.34s ================================

Resultat:


3. Teil 2: Test-Coverage - Welcher Code ist getestet?

3.1. Das Problem: “Alle Tests bestehen” ≠ “Aller Code ist getestet”

Szenario: Ihre CI ist grün. Alle 47 Tests bestehen. Sie fühlen sich sicher.

$ pytest tests/
================================ 47 passed in 2.34s ==================================

Aber dann: Ein Benutzer meldet einen Bug in Produktion:

# Bug Report: Crash when angle > 90 degrees
# Code: geometry.py, line 78
# Error: ValueError: math domain error in tan()

Sie schauen sich den Code an:

def find_intersection(x_road, y_road, angle_degrees, camera_x=0, camera_y=1.5):
    """Find intersection of camera ray with road profile."""

    if len(x_road) == 0 or len(y_road) == 0:
        return None

    # 🐛 BUG: Keine Validierung für angle_degrees > 90
    # tan(90°) = undefiniert (vertikaler Strahl)
    angle_rad = -np.deg2rad(angle_degrees)
    slope = np.tan(angle_rad)  # 💥 Crash wenn angle = 90

    # ... Rest der Funktion ...

Sie checken Ihre Tests:

# tests/test_geometry.py
def test_find_intersection_normal_angle():
    """Test with normal viewing angle."""
    result = find_intersection(x_road, y_road, angle_degrees=-5.0)
    assert result is not None

def test_find_intersection_steep_angle():
    """Test with steeper viewing angle."""
    result = find_intersection(x_road, y_road, angle_degrees=-15.0)
    assert result is not None

def test_find_intersection_shallow_angle():
    """Test with shallow viewing angle."""
    result = find_intersection(x_road, y_road, angle_degrees=-2.0)
    assert result is not None

Das Problem:

Die Frage:

“Wie viel von meinem Code wird eigentlich von meinen Tests ausgeführt?”

Die Antwort: Test-Coverage


3.2. Was ist Test-Coverage?

Definition: Test-Coverage (oder Code-Coverage) ist ein Maß, das angibt, welcher Prozentsatz Ihres Codes von Ihren Tests ausgeführt wird.

Formel:

\[\text{Coverage} = \frac{\text{Anzahl der ausgeführten Zeilen}}{\text{Gesamtanzahl der Zeilen}} \times 100\%\]

Beispiel:

# geometry.py (10 Zeilen Code)
def find_intersection(x_road, y_road, angle_degrees, camera_x=0, camera_y=1.5):
    # Zeile 1: if len(x_road) == 0:
    if len(x_road) == 0 or len(y_road) == 0:
        # Zeile 2: return None
        return None

    # Zeile 3: angle_rad = ...
    angle_rad = -np.deg2rad(angle_degrees)
    # Zeile 4: slope = ...
    slope = np.tan(angle_rad)
    # Zeile 5: intercept = ...
    intercept = camera_y - slope * camera_x

    # Zeile 6-10: ... Rest der Funktion ...

Wenn Ihre Tests nur Zeilen 1, 3, 5, 6-10 ausführen:

\[\text{Coverage} = \frac{8}{10} \times 100\% = 80\%\]

Was bedeutet das:

Vertiefung (Kapitel 03 (Testtheorie und Coverage)): In Kapitel 03 (Testtheorie und Coverage) werden wir die theoretischen Grundlagen von Coverage erkunden. Sie werden lernen, dass Coverage immer “Coverage eines Modells” ist - und es gibt verschiedene Modelle (Kontrollfluss-Graphen, Input-Domain-Partitionen), die zu verschiedenen Coverage-Kriterien führen (Statement, Branch, Path, etc.). Verstehen Sie für jetzt einfach, dass Coverage Ihnen sagt “welcher Prozentsatz des Codes während der Tests ausgeführt wurde.”


3.3. Arten von Coverage

Es gibt mehrere Arten von Coverage-Metriken:

1. Line Coverage (Zeilen-Coverage)

Fragt: “Wurde diese Zeile ausgeführt?”

# ✅ Zeile ausgeführt
result = find_intersection(x_road, y_road, angle_degrees=-5.0)

# ❌ Zeile nie ausgeführt
if angle_degrees > 90:
    raise ValueError("Angle too large")

Am häufigsten verwendet, einfach zu messen.

2. Branch Coverage (Zweig-Coverage)

Fragt: “Wurden beide Zweige (if/else) ausgeführt?”

if len(x_road) == 0:
    return None  # ❓ Wurde dieser Zweig getestet?
else:
    # Berechnung fortsetzen  # ❓ Wurde dieser Zweig getestet?

Beispiel:

# Test 1: Leeres Array
def test_empty_array():
    result = find_intersection([], [], -5.0)
    assert result is None  # ✅ Testet "if"-Zweig

# Test 2: Nicht-leeres Array
def test_normal_array():
    result = find_intersection([0, 1], [0, 0], -5.0)
    assert result is not None  # ✅ Testet "else"-Zweig

Branch Coverage: 100% (beide Zweige getestet)

3. Function Coverage (Funktions-Coverage)

Fragt: “Wurde diese Funktion aufgerufen?”

# ✅ Funktion getestet
def find_intersection(...):
    pass

# ❌ Funktion nie aufgerufen
def calculate_curvature(...):
    pass

In diesem Kurs: Wir konzentrieren uns auf Line Coverage, da es am einfachsten zu verstehen und zu messen ist.


3.4. Tool: coverage.py und pytest-cov

Python hat zwei beliebte Coverage-Tools:

  1. coverage.py: Low-Level-Tool zum Messen von Coverage
  2. pytest-cov: Pytest-Plugin, das coverage.py integriert

Wir verwenden pytest-cov weil:


3.5. Installation und Verwendung von pytest-cov

Schritt 1: Installiere pytest-cov

# Mit uv (empfohlen für diesen Kurs)
$ uv add --dev pytest-cov

# Mit pip
$ pip install pytest-cov

Schritt 2: Führe Tests mit Coverage aus

$ uv run pytest tests/ --cov=code_examples

Optionen:

Beispielausgabe:

============================= test session starts ==============================
collected 47 items

tests/test_geometry.py ......                                            [ 12%]
tests/test_road.py .........                                             [ 31%]
tests/test_visualization.py ....                                         [ 40%]
tests/test_integration.py ..........                                     [ 61%]
tests/test_edge_cases.py ..................                              [100%]

---------- coverage: platform linux, python 3.12.0 -----------
Name                              Stmts   Miss  Cover   Missing
---------------------------------------------------------------
code_examples/__init__.py             0      0   100%
code_examples/geometry.py            45      5    89%   78-82
code_examples/road.py                38      2    95%   102, 115
code_examples/visualization.py       52      8    85%   67-74
code_examples/main.py                23      5    78%   45-49
---------------------------------------------------------------
TOTAL                               158     20    87%

============================= 47 passed in 2.67s ================================

Was bedeutet das:

Spalte Bedeutung
Stmts Anzahl der Code-Zeilen (Statements)
Miss Anzahl der Zeilen, die nie ausgeführt wurden
Cover Coverage-Prozentsatz = (Stmts - Miss) / Stmts × 100%
Missing Zeilennummern, die nicht abgedeckt sind

Beispiel:


3.6. Beispiel: Coverage-Bericht interpretieren

Szenario: Sie schaust auf den Coverage-Bericht und siehst:

Name                    Stmts   Miss  Cover   Missing
-----------------------------------------------------
code_examples/geometry.py   45      5    89%   78-82

Schritt 1: Schauen Sie sich die vermissten Zeilen an

# code_examples/geometry.py
def find_intersection(x_road, y_road, angle_degrees, camera_x=0, camera_y=1.5):
    """Find intersection of camera ray with road profile."""

    if len(x_road) == 0 or len(y_road) == 0:
        return None

    # Zeilen 78-82: NIE AUSGEFÜHRT
    if abs(angle_degrees) >= 90:
        raise ValueError(
            f"Invalid angle: {angle_degrees}. "
            "Angle must be between -90 and 90 degrees."
        )

    angle_rad = -np.deg2rad(angle_degrees)
    slope = np.tan(angle_rad)
    # ... Rest ...

Schritt 2: Verstehen, warum sie vermisst werden

Sie checken Ihre Tests:

# tests/test_geometry.py

# ✅ Getestet: Normale Winkel
def test_find_intersection_normal_angle():
    result = find_intersection(x_road, y_road, angle_degrees=-5.0)
    assert result is not None

# ❌ NICHT GETESTET: Winkel >= 90 Grad
# (Deswegen Zeilen 78-82 vermisst werden)

Schritt 3: Schreibe einen Test für den vermissten Code

# tests/test_geometry.py

def test_find_intersection_invalid_angle():
    """Test that angles >= 90 degrees raise ValueError."""
    x_road = np.array([0, 1, 2])
    y_road = np.array([0, 0, 0])

    # Test angle = 90 degrees
    with pytest.raises(ValueError, match="Invalid angle"):
        find_intersection(x_road, y_road, angle_degrees=90)

    # Test angle = -90 degrees
    with pytest.raises(ValueError, match="Invalid angle"):
        find_intersection(x_road, y_road, angle_degrees=-90)

    # Test angle > 90 degrees
    with pytest.raises(ValueError, match="Invalid angle"):
        find_intersection(x_road, y_road, angle_degrees=120)

Schritt 4: Führe Tests erneut mit Coverage aus

$ uv run pytest tests/ --cov=code_examples --cov-report=term-missing

Neue Ausgabe:

Name                    Stmts   Miss  Cover   Missing
-----------------------------------------------------
code_examples/geometry.py   45      0   100%

Resultat:


3.7. HTML Coverage-Bericht

Für eine bessere Visualisierung können Sie einen HTML-Bericht generieren:

$ uv run pytest tests/ --cov=code_examples --cov-report=html

Das erstellt:

htmlcov/
├── index.html           # Hauptbericht
├── geometry_py.html     # Detaillierter Bericht für geometry.py
├── road_py.html         # Detaillierter Bericht für road.py
└── ...

Öffnen Sie htmlcov/index.html in Ihrem Browser:

# Windows
$ start htmlcov/index.html

# macOS
$ open htmlcov/index.html

# Linux
$ xdg-open htmlcov/index.html

Sie werden sehen:

Beispiel-Screenshot (konzeptuell):

Coverage Report
───────────────────────────────────────────
File                    Stmts   Miss  Cover
───────────────────────────────────────────
geometry.py               45      5    89%  [View Details]
road.py                   38      2    95%  [View Details]
visualization.py          52      8    85%  [View Details]
───────────────────────────────────────────
TOTAL                    158     20    87%

Detaillierte Ansicht (geometry.py):

   1  import numpy as np
   2
   3  def find_intersection(x_road, y_road, angle_degrees, camera_x=0, camera_y=1.5):
   4      """Find intersection of camera ray with road profile."""
   5
   6      if len(x_road) == 0 or len(y_road) == 0:   Abgedeckt
   7          return None                             Abgedeckt
   8
   9      if abs(angle_degrees) >= 90:                Nicht abgedeckt (Zeile 78)
  10          raise ValueError(...)                   Nicht abgedeckt (Zeile 79)

Grün = Von Tests ausgeführt Rot = Nie ausgeführt


4. Teil 3: Test-Coverage zu CI hinzufügen

4.1. Warum Coverage in CI messen?

Problem: Sie führen Coverage lokal aus und siehst 87% Coverage. Gut!

Aber:

Lösung: Automatisiere Coverage-Messung in CI

Vorteile:

  1. Sichtbarkeit: Jeder PR zeigt Coverage-Änderung
  2. Rechenschaftspflicht: Entwickler sehen, wenn sie Coverage senken
  3. Trendanalyse: Coverage-Verlauf im Zeitverlauf verfolgen
  4. Durchsetzung: Optional Coverage-Mindestanforderungen setzen

4.2. Aktualisieren deines CI-Workflows mit Coverage

Aktualisierte .github/workflows/quality.yml:

name: Code Quality Checks

on:
  pull_request:
    branches: [main]
  push:
    branches: [main]

jobs:
  quality:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.12'

      - name: Install uv
        uses: astral-sh/setup-uv@v5
        with:
          version: "latest"

      - name: Install dependencies
        run: uv sync

      - name: Run Ruff linter
        run: uv run ruff check code_examples/

      - name: Run Ruff formatter
        run: uv run ruff format --check code_examples/

      - name: Run Pyright
        run: uv run pyright code_examples/

      # 🆕 AKTUALISIERT: Führe Tests mit Coverage aus
      - name: Run tests with coverage
        run: |
          uv run pytest tests/ \
            --cov=code_examples \
            --cov-report=term-missing \
            --cov-report=html \
            --cov-fail-under=70

      # 🆕 NEU: Lade Coverage-Bericht als Artefakt hoch
      - name: Upload coverage report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: coverage-report
          path: htmlcov/

Neue Optionen:


4.3. Coverage-Schwellenwert: –cov-fail-under

Option: --cov-fail-under=70

Was es macht:

Beispiel:

# Coverage = 87% → ERFOLG
$ pytest tests/ --cov=code_examples --cov-fail-under=70
...
TOTAL                    158     20    87%
============================= 47 passed in 2.67s ================================
$ echo $?
0  # Exit-Code 0 = Erfolg

# Coverage = 65% → FEHLER
$ pytest tests/ --cov=code_examples --cov-fail-under=70
...
TOTAL                    158     55    65%
FAIL Required test coverage of 70% not reached. Total coverage: 65.00%
============================= 47 passed in 2.67s ================================
$ echo $?
1  # Exit-Code 1 = Fehler

In CI:

Empfohlene Schwellenwerte:

Projekt-Typ Empfohlene Coverage
Neue Projekte 70-80%
Kritische Systeme 80-90%
Legacy-Code 50-60% (schrittweise erhöhen)
Libraries 90%+

Für diesen Kurs: 70% ist ein gutes Ziel.

Vorschau (Kapitel 03 (Testtheorie und Coverage)): Das Coverage-Tool meldet “Line Coverage” (auch Statement Coverage oder C0 genannt). In Kapitel 03 (Testtheorie und Coverage) werden Sie “Branch Coverage” (C1) kennenlernen, das stärker ist - es erfordert das Testen sowohl der True- als auch der False-Ergebnisse jeder Entscheidung. Branch Coverage gibt mehr Vertrauen, erfordert aber mehr Tests.


4.4. Coverage-Bericht als Artefakt hochladen

Warum: HTML-Coverage-Berichte sind großartig für die Visualisierung, aber sie sind nicht in CI-Logs sichtbar.

Lösung: Lade den HTML-Bericht als GitHub Actions-Artefakt hoch.

Workflow-Schritt:

- name: Upload coverage report
  if: always()  # Auch hochladen, wenn Tests fehlschlagen
  uses: actions/upload-artifact@v4
  with:
    name: coverage-report
    path: htmlcov/

Zugriff auf den Bericht:

  1. Gehen Sie zu Ihrem PR auf GitHub
  2. Scrolle zu ChecksCode Quality Checks
  3. Klicke auf Summary
  4. Unter Artifactscoverage-reportDownload
  5. Entpacke und öffne htmlcov/index.html

Nutzen:


4.5. Beispiel: PR mit Coverage-Bericht

Szenario: Sie erstellen einen PR, der neuen Code hinzufügt, aber keine Tests.

Workflow-Ausgabe:

Run tests with coverage
============================= test session starts ==============================
collected 47 items

tests/test_geometry.py ......                                            [ 12%]
tests/test_road.py .........                                             [ 31%]
tests/test_visualization.py ....                                         [ 40%]
tests/test_integration.py ..........                                     [ 61%]
tests/test_edge_cases.py ..................                              [100%]

---------- coverage: platform linux, python 3.12.0 -----------
Name                    Stmts   Miss  Cover   Missing
-----------------------------------------------------
code_examples/geometry.py   50      8    84%   78-82, 95-97
code_examples/road.py       38      2    95%   102, 115
-----------------------------------------------------
TOTAL                       158     25    84%

FAIL Required test coverage of 70% not reached. Total coverage: 84.00%
Error: Process completed with exit code 1.

Warte, was? Coverage ist 84%, aber es schlägt fehl?

Erklärung:

Richtiges Beispiel (Coverage zu niedrig):

TOTAL                       158     55    65%

FAIL Required test coverage of 70% not reached. Total coverage: 65.00%

Resultat:


5. Teil 4: Was Coverage NICHT ist

5.1. Häufiges Missverständnis: “100% Coverage = Fehlerfreier Code”

Mythos:

“Wenn ich 100% Coverage habe, hat mein Code keine Bugs.”

Realität:

“100% Coverage bedeutet, dass alle Zeilen ausgeführt wurden, nicht dass sie korrekt sind.”

Beispiel:

# code_examples/geometry.py
def calculate_distance(x1, y1, x2, y2):
    """Calculate Euclidean distance between two points."""
    # 🐛 BUG: Sollte (x2-x1)**2 + (y2-y1)**2 sein, nicht (x2+x1)**2
    return ((x2 + x1)**2 + (y2 - y1)**2) ** 0.5

Test:

# tests/test_geometry.py
def test_calculate_distance():
    """Test distance calculation."""
    result = calculate_distance(0, 0, 3, 4)
    # ❌ FALSCHER TEST: Erwartet falsches Ergebnis
    assert result == 5.0  # Zufällig korrekt für (0,0) → (3,4)

Coverage:

$ pytest tests/ --cov=code_examples
Name                    Stmts   Miss  Cover
-------------------------------------------
code_examples/geometry.py    3      0   100%  ✅ 100% Coverage

Aber der Code hat einen Bug!

# Wenn wir einen anderen Test versuchen:
def test_calculate_distance_different_points():
    result = calculate_distance(1, 1, 4, 5)
    # Erwartet: sqrt((4-1)^2 + (5-1)^2) = sqrt(9 + 16) = 5.0
    # Tatsächlich: sqrt((4+1)^2 + (5-1)^2) = sqrt(25 + 16) = 6.4 ❌
    assert result == 5.0  # SCHLÄGT FEHL

Lektion:

Vertiefung (Kapitel 03 (Testtheorie und Coverage)): Wir werden diese Einschränkungen in Kapitel 03 (Testtheorie und Coverage) viel detaillierter erkunden, einschließlich spezifischer Beispiele, wie 100% Coverage immer noch Bugs übersehen kann, und wie Coverage mit formalen Anforderungen zusammenhängt.


5.2. Coverage ist ein Werkzeug, kein Ziel

Gefährliche Denkweise:

“Unser Ziel ist 100% Coverage. Schreibe Tests, um diese Zahl zu erreichen.”

Bessere Denkweise:

“Unser Ziel ist zuverlässiger Code. Coverage hilft uns, ungetestete Bereiche zu identifizieren.”

Beispiel schlechter Praxis:

# Nur geschrieben, um Coverage zu erhöhen
def test_useless():
    """Dieser Test erhöht Coverage, überprüft aber nichts."""
    result = find_intersection([0, 1], [0, 0], -5.0)
    # Keine Assertions! Nur Coverage 📈

Resultat:

Beispiel guter Praxis:

def test_find_intersection_normal_case():
    """Test that intersection is found for valid inputs."""
    x_road = np.array([0, 1, 2, 3, 4])
    y_road = np.array([0, 0, 0, 0, 0])

    result = find_intersection(x_road, y_road, angle_degrees=-5.0)

    # ✅ Klare Assertions
    assert result is not None, "Should find intersection"
    assert 0 <= result <= 4, "Intersection should be within road bounds"

Resultat:


5.3. Das Goodhart-Gesetz

Goodhart-Gesetz:

“Wenn ein Maß zu einem Ziel wird, hört es auf, ein gutes Maß zu sein.”

Angewendet auf Coverage:

Anzeichen, dass Sie Coverage als Ziel behandeln:

Richtiger Ansatz:

  1. Schreibe sinnvolle Tests basierend auf Anforderungen
  2. Verwende Coverage, um Lücken zu finden
  3. Analysiere Lücken: Braucht dieser Code einen Test?
  4. Füge Tests hinzu, wo sie Wert hinzufügen
  5. Akzeptiere < 100% wenn es sinnvoll ist

5.4. Wann 100% Coverage NICHT das Ziel ist

Szenarien, in denen < 100% Coverage in Ordnung ist:

1. Trivialer Code

# Getter/Setter - Kein Geschäftslogik
@property
def camera_x(self):
    return self._camera_x

@camera_x.setter
def camera_x(self, value):
    self._camera_x = value

Abdecken? Vielleicht nicht. Kein Wert.

2. Defensive Fehlerbehandlung

def process_data(data):
    try:
        return data.process()
    except AttributeError:  # Sollte nie passieren
        # Defensive Programmierung, für den Fall eines unerwarteten Bugs
        logger.error("Unexpected: data has no process() method")
        return None

Abdecken? Schwer zu testen, niedriger ROI.

3. Legacy-Code

# Alter Code, den niemand versteht, aber der funktioniert
def mysterious_calculation(x):
    return ((x * 17) % 42) + (x // 7) * 3

Abdecken? Vielleicht später, wenn Zeit vorhanden ist.

4. Debugging-Code

if DEBUG:
    print(f"Debug: x={x}, y={y}, angle={angle}")

Abdecken? Nein, Debug-Modus ist nicht für Tests.

Pragmatischer Ansatz:


6. Teil 5: Test-Driven Development (TDD)

6.1. Was ist TDD?

Definition: Test-Driven Development (TDD) ist eine Softwareentwicklungspraxis, bei der Sie Tests schreiben BEVOR Sie Implementierungscode schreiben.

Herkömmlicher Ansatz:

1. Schreibe Code
2. Teste Code (manuell oder mit Tests)
3. Fixe Bugs
4. Wiederhole

TDD-Ansatz:

1. Schreibe einen Test (schlägt fehl)
2. Schreibe gerade genug Code, um den Test zu bestehen
3. Refaktoriere Code
4. Wiederhole

Kernprinzip:

“Sie schreiben keinen Produktionscode, außer um einen fehlgeschlagenen Test zu bestehen.”


6.2. Der Red-Green-Refactor-Zyklus

TDD folgt einem strengen Zyklus:

graph LR
    A[🔴 Red<br/>Schreibe fehlgeschlagenen Test] --> B[🟢 Green<br/>Schreibe Code, um zu bestehen]
    B --> C[🔵 Refactor<br/>Verbessere Code]
    C --> A

Phase 1: 🔴 Red (Rot)

Phase 2: 🟢 Green (Grün)

Phase 3: 🔵 Refactor (Refaktorieren)

Wiederhole diesen Zyklus für jede neue Funktion.


6.3. TDD-Beispiel: Schreibe validate_angle()

Szenario: Sie wollen Winkel-Validierung zu geometry.py hinzufügen.

Anforderung:

Traditioneller Ansatz:

# 1. Schreibe Funktion
def validate_angle(angle_degrees):
    if abs(angle_degrees) >= 90:
        raise ValueError(f"Invalid angle: {angle_degrees}")

# 2. Teste manuell
validate_angle(45)  # OK
validate_angle(90)  # Sollte werfen... macht es?

TDD-Ansatz: Folge Red-Green-Refactor.


Iteration 1: Validiere normalen Winkel

🔴 RED: Schreibe fehlgeschlagenen Test

# tests/test_geometry.py
import pytest
from code_examples.geometry import validate_angle

def test_validate_angle_normal():
    """Test that normal angles are accepted."""
    validate_angle(45)  # Sollte nicht werfen
    validate_angle(-45)  # Sollte nicht werfen
    validate_angle(0)  # Sollte nicht werfen

Führe Test aus:

$ pytest tests/test_geometry.py::test_validate_angle_normal
...
ImportError: cannot import name 'validate_angle' from 'code_examples.geometry'
FAILED

✅ Gut! Test schlägt fehl (Funktion existiert nicht).


🟢 GREEN: Schreibe minimalen Code

# code_examples/geometry.py
def validate_angle(angle_degrees):
    pass  # Tut nichts, aber lässt Test bestehen

Führe Test aus:

$ pytest tests/test_geometry.py::test_validate_angle_normal
...
PASSED ✅

✅ Test besteht! (Weil kein raise → keine Ausnahme → Test besteht)


🔵 REFACTOR: Nichts zu refaktorieren (Code ist trivial)


Iteration 2: Validiere ungültige Winkel

🔴 RED: Schreibe fehlgeschlagenen Test

# tests/test_geometry.py
def test_validate_angle_invalid():
    """Test that invalid angles raise ValueError."""

    with pytest.raises(ValueError, match="Invalid angle: 90"):
        validate_angle(90)

    with pytest.raises(ValueError, match="Invalid angle: -90"):
        validate_angle(-90)

    with pytest.raises(ValueError, match="Invalid angle: 120"):
        validate_angle(120)

Führe Test aus:

$ pytest tests/test_geometry.py::test_validate_angle_invalid
...
FAILED (test did not raise ValueError)

✅ Gut! Test schlägt fehl (Funktion wirft nicht).


🟢 GREEN: Schreibe minimalen Code

# code_examples/geometry.py
def validate_angle(angle_degrees):
    if abs(angle_degrees) >= 90:
        raise ValueError(f"Invalid angle: {angle_degrees}")

Führe Test aus:

$ pytest tests/test_geometry.py::test_validate_angle_invalid
...
PASSED ✅

✅ Test besteht!


🔵 REFACTOR: Code verbessern

# code_examples/geometry.py
def validate_angle(angle_degrees):
    """Validate that angle is within valid range (-90, 90).

    Args:
        angle_degrees: Viewing angle in degrees

    Raises:
        ValueError: If angle is outside valid range
    """
    if abs(angle_degrees) >= 90:
        raise ValueError(
            f"Invalid angle: {angle_degrees}. "
            "Angle must be between -90 and 90 degrees."
        )

Führe alle Tests aus:

$ pytest tests/test_geometry.py
...
PASSED ✅ (beide Tests bestehen)

✅ Refactoring abgeschlossen, Tests sind immer noch grün.


Iteration 3: Validiere Grenzwerte

🔴 RED: Schreibe fehlgeschlagenen Test

# tests/test_geometry.py
def test_validate_angle_boundary():
    """Test boundary values."""

    # 89 sollte OK sein
    validate_angle(89)
    validate_angle(-89)

    # 89.9 sollte OK sein
    validate_angle(89.9)
    validate_angle(-89.9)

    # 90.0 sollte werfen
    with pytest.raises(ValueError):
        validate_angle(90.0)

Führe Test aus:

$ pytest tests/test_geometry.py::test_validate_angle_boundary
...
PASSED ✅

Warte, es hat bestanden?

Ja! Unser bestehender Code behandelt bereits Grenzwerte korrekt.

Das ist TDD in Aktion:


🔵 REFACTOR: Nichts zu ändern

Code ist bereits sauber. Fertig!


6.4. Vorteile von TDD

1. Design verbessern

2. Sofortiges Feedback

3. Bessere Coverage

4. Weniger Debugging

5. Vertrauen beim Refactoring

6. Lebendige Dokumentation


6.5. Herausforderungen von TDD

1. Anfängliche Lernkurve

2. Zeitaufwand

3. Overhead für prototyping

4. Schwierig für Legacy-Code

Pragmatische Lösung:


6.6. TDD in der Praxis: Wann es zu verwenden ist

Verwende TDD:

Überspringe TDD:

Hybridansatz (empfohlen):

1. Explorative Phase: Schreibe Code ohne Tests (schnell experimentieren)
2. Stabilisierungsphase: Sobald klar, was funktioniert → schreibe Tests
3. Feature-Phase: Verwende TDD für neue Features
4. Wartungsphase: Verwende TDD für Bug-Fixes

Verbindung zu Kapitel 03 (Testtheorie und Coverage): TDD tendiert natürlich dazu, hohe Coverage zu produzieren, weil jede Codezeile geschrieben wird, um einen Test zu bestehen. In Kapitel 03 (Testtheorie und Coverage) werden Sie die formale Beziehung lernen: TDD produziert Test-Suites, die für mehrere Coverage-Kriterien gleichzeitig “adäquat” sind.


7. Teil 6: TDD + Coverage + CI zusammenführen

7.1. Der komplette professionelle Workflow

Bringen wir alles zusammen:

graph TD
    A[Neue Feature-Anforderung] --> B{TDD verwenden?}
    B -->|Ja - Kritische Logik| C[🔴 RED: Schreibe fehlgeschlagenen Test]
    B -->|Nein - Einfaches Feature| D[Schreibe Code]

    C --> E[🟢 GREEN: Schreibe minimalen Code]
    E --> F[🔵 REFACTOR: Verbessere Code]
    F --> G[Führe alle Tests lokal aus]

    D --> H[Schreibe Tests nach]
    H --> G

    G --> I[Prüfe Coverage lokal]
    I --> J{Coverage >= 70%?}
    J -->|Nein| K[Füge mehr Tests hinzu]
    K --> G

    J -->|Ja| L[Commit + Push]
    L --> M[GitHub Actions CI läuft]
    M --> N{Alle Checks bestehen?}

    N -->|Nein| O[Checke CI-Logs]
    O --> P{Was ist fehlgeschlagen?}
    P -->|Tests| Q[Fixe Tests]
    P -->|Coverage| R[Füge Tests hinzu]
    P -->|Linter| S[Fixe Stil]
    Q --> L
    R --> L
    S --> L

    N -->|Ja| T[Erstelle PR]
    T --> U[Code Review]
    U --> V{Genehmigt?}
    V -->|Nein| W[Adressiere Feedback]
    W --> L
    V -->|Ja| X[Merge zu main]
    X --> Y[Feature abgeschlossen! 🎉]

7.2. Hands-On-Beispiel: Kompletter Workflow

Szenario: Sie willst eine neue Funktion hinzufügen: calculate_curvature().

Anforderung:


Schritt 1: Feature-Branch erstellen

$ git checkout main
$ git pull origin main
$ git checkout -b feature/add-curvature-calculation

Schritt 2: TDD - 🔴 RED (Schreibe fehlgeschlagenen Test)

# tests/test_road.py
import numpy as np
from code_examples.road import calculate_curvature

def test_calculate_curvature_straight_road():
    """Test curvature of straight road is zero."""
    x_road = np.array([0, 1, 2, 3, 4])
    y_road = np.array([0, 0, 0, 0, 0])  # Gerade Linie

    curvature = calculate_curvature(x_road, y_road, index=2)

    assert abs(curvature) < 0.01, "Straight road should have zero curvature"

def test_calculate_curvature_curved_road():
    """Test curvature of curved road is non-zero."""
    x_road = np.array([0, 1, 2, 3, 4])
    y_road = np.array([0, 1, 0, -1, 0])  # Gebogene Linie

    curvature = calculate_curvature(x_road, y_road, index=2)

    assert curvature != 0, "Curved road should have non-zero curvature"

Führe Tests aus:

$ uv run pytest tests/test_road.py::test_calculate_curvature_straight_road
...
ImportError: cannot import name 'calculate_curvature' from 'code_examples.road'
FAILED ❌

✅ Gut! Test schlägt fehl.


Schritt 3: TDD - 🟢 GREEN (Schreibe minimalen Code)

# code_examples/road.py
import numpy as np

def calculate_curvature(x_road, y_road, index):
    """Calculate road curvature at given index using finite differences."""

    if index < 1 or index >= len(x_road) - 1:
        return 0.0  # Kann Krümmung nicht an Endpunkten berechnen

    # Zweite Ableitung über finite Differenzen
    dx = x_road[index + 1] - x_road[index - 1]
    dy = y_road[index + 1] - y_road[index - 1]
    ddx = x_road[index + 1] - 2 * x_road[index] + x_road[index - 1]
    ddy = y_road[index + 1] - 2 * y_road[index] + y_road[index - 1]

    # Krümmung = |x'y'' - y'x''| / (x'^2 + y'^2)^(3/2)
    numerator = abs(dx * ddy - dy * ddx)
    denominator = (dx**2 + dy**2)**1.5

    if denominator < 1e-10:
        return 0.0

    return numerator / denominator

Führe Tests aus:

$ uv run pytest tests/test_road.py::test_calculate_curvature_straight_road
...
PASSED ✅

$ uv run pytest tests/test_road.py::test_calculate_curvature_curved_road
...
PASSED ✅

✅ Tests bestehen!


Schritt 4: TDD - 🔵 REFACTOR (Verbessere Code)

Code sieht gut aus, aber fügen wir Docstring hinzu:

def calculate_curvature(x_road, y_road, index):
    """Calculate road curvature at given index.

    Uses finite difference approximation of second derivative.

    Args:
        x_road: Array of x-coordinates
        y_road: Array of y-coordinates
        index: Index at which to calculate curvature

    Returns:
        Curvature value (0 for straight road, > 0 for curves)

    Note:
        Returns 0 for edge points (index 0 or len-1)
    """
    # ... Rest des Codes ...

Führe alle Tests aus:

$ uv run pytest tests/
...
47 passed ✅

✅ Refactoring abgeschlossen, alle Tests grün.


Schritt 5: Prüfe Coverage lokal

$ uv run pytest tests/ --cov=code_examples --cov-report=term-missing

Ausgabe:

Name                    Stmts   Miss  Cover   Missing
-----------------------------------------------------
code_examples/road.py      42      0   100%
-----------------------------------------------------
TOTAL                     158      0   100%

✅ 100% Coverage!


Schritt 6: Commit und Push

$ git add code_examples/road.py tests/test_road.py
$ git commit -m "Add calculate_curvature() with tests"
$ git push origin feature/add-curvature-calculation

Schritt 7: Beobachte GitHub Actions CI

Workflow läuft automatisch:

Code Quality Checks
├─ Checkout code          ✅ PASSED
├─ Set up Python          ✅ PASSED
├─ Install uv             ✅ PASSED
├─ Install dependencies   ✅ PASSED
├─ Run Ruff linter        ✅ PASSED
├─ Run Ruff formatter     ✅ PASSED
├─ Run Pyright            ✅ PASSED
└─ Run tests with coverage ✅ PASSED
   ├─ 49 tests passed
   └─ Coverage: 100%

✅ Alle Checks grün!


Schritt 8: Erstelle Pull Request

Auf GitHub:

  1. Gehen Sie zu Ihrem Repository
  2. Klicke “Pull requests” → “New pull request”
  3. Wähle feature/add-curvature-calculationmain
  4. Titel: “Add road curvature calculation”
  5. Beschreibung:
## Summary
Adds `calculate_curvature()` function to calculate road curvature at a given point.

## Changes
- Added `calculate_curvature()` to `road.py`
- Added unit tests for straight and curved roads
- Coverage: 100%

## Testing
- ✅ All tests pass (49 total)
- ✅ Coverage: 100%
- ✅ Linter: No issues
  1. Klicke “Create pull request”

Schritt 9: Code Review

Ihr Teamkollege reviewed:

✅ Sieht gut aus! Tests sind gründlich.
✅ Coverage ist ausgezeichnet.
💡 Eine Frage: Was passiert wenn x_road und y_road unterschiedliche Längen haben?

Sie antworten:

# Füge Validierung hinzu
def calculate_curvature(x_road, y_road, index):
    """..."""

    # Neu: Validiere Eingabe
    if len(x_road) != len(y_road):
        raise ValueError("x_road and y_road must have same length")

    if index < 1 or index >= len(x_road) - 1:
        return 0.0

    # ... Rest des Codes ...

Füge Test hinzu:

def test_calculate_curvature_mismatched_lengths():
    """Test that mismatched array lengths raise ValueError."""
    x_road = np.array([0, 1, 2])
    y_road = np.array([0, 1])  # Kürzeres Array

    with pytest.raises(ValueError, match="same length"):
        calculate_curvature(x_road, y_road, index=1)

Commit und Push:

$ git add code_examples/road.py tests/test_road.py
$ git commit -m "Add validation for mismatched array lengths"
$ git push origin feature/add-curvature-calculation

CI läuft erneut → ✅ Grün


Schritt 10: Merge

Reviewer genehmigt:

✅ Approved! Bereit zum Merge.

Sie klicken “Merge pull request”

GitHub Actions läuft auf main:

Code Quality Checks (main branch)
└─ All checks passed ✅

✅ Feature abgeschlossen!


8. Best Practices und Takeaways

8.1. Best Practices für Tests in CI

1. Mache Tests obligatorisch

2. Halte Tests schnell

3. Führe Tests bei jedem Push aus

4. Zeige Coverage-Trends

5. Schläge nicht bei kleinen Coverage-Rückgängen fehl


8.2. Best Practices für Coverage

1. Setze realistische Ziele

2. Fokussiere auf Qualität, nicht Quantität

3. Verwende Coverage, um Lücken zu finden

4. Dokumentiere warum Coverage < 100% ist


8.3. Best Practices für TDD

1. Starte klein

2. Ein Test auf einmal

3. Halte Tests isoliert

4. Verwende beschreibende Test-Namen

5. Teste Verhalten, nicht Implementierung


8.4. Was Sie in dieser Vorlesung gelernt haben

1. Tests + CI

2. Test-Coverage

3. Test-Driven Development (TDD)

4. Professioneller Workflow


8.5. Übung: Wende es auf Road Profile Viewer an

Ihre Aufgabe:

  1. Aktiviere Coverage in CI
    • Aktualisiere .github/workflows/quality.yml
    • Füge --cov=code_examples --cov-fail-under=70 hinzu
  2. Prüfe aktuelle Coverage
    • Führe pytest --cov=code_examples --cov-report=html aus
    • Öffne htmlcov/index.html
    • Identifiziere vermisste Zeilen
  3. Füge Tests für vermisste Zeilen hinzu
    • Wähle eine Datei mit < 80% Coverage
    • Schreibe Tests, um vermisste Zeilen abzudecken
  4. Versuche TDD für eine neue Funktion
    • Wähle ein kleines Feature (z.B. validate_road_data())
    • Folge Red-Green-Refactor
    • Committe nach jedem Zyklus
  5. Erstelle einen PR
    • Pushen Sie Ihren Branch
    • Beobachte CI laufen
    • Sehe Coverage in GitHub Actions

9. Zusammenfassung

Was Sie gelernt haben:

  1. Tests in CI sind obligatorisch
    • Tests lokal laufen lassen ≠ Tests werden immer ausgeführt
    • GitHub Actions + Branch Protection Rules erzwingen Tests
  2. Coverage ist ein Werkzeug, kein Ziel
    • Misst, welcher Code von Tests ausgeführt wird
    • 70-80% Coverage ist ein gutes Ziel für die meisten Projekte
    • 100% Coverage ≠ fehlerfreier Code
  3. TDD ist eine hilfreiche Disziplin
    • Red-Green-Refactor-Zyklus
    • Schreibe Tests zuerst, Code danach
    • Optional, aber leistungsstark für kritische Logik
  4. Der komplette Workflow
    • Feature-Branch → TDD → Coverage → CI → PR → Review → Merge
    • Tests, Coverage und CI arbeiten zusammen, um Qualität sicherzustellen
    • Automatisierung gibt Ihnen Vertrauen, schnell zu shippen

Nächste Schritte:

Sie haben jetzt:

Glückwunsch! 🎉 Sie haben die volle CI/CD + Testing Pipeline gemeistert!


10. Weitere Lektüre

Coverage:

TDD:

GitHub Actions:

Best Practices:

Kommt als Nächstes: Kapitel 03 (Testtheorie und Coverage) - Testing-Theorie

In Kapitel 03 (Testtheorie und Coverage) tauchen wir tief in die theoretischen Grundlagen ein:

Diese Theorie wird Ihnen helfen zu verstehen, warum die praktischen Techniken, die Sie heute gelernt haben, tatsächlich funktionieren.

© 2026 Dominik Mueller   •  Powered by Soopr   •  Theme  Moonwalk