Kapitel 03

CODE COVERAGE: C0 & C1 BERECHNEN

Software Engineering - SoSe 2026

Am Ende dieser Einheit können Sie:

  1. C0 (Statement) und C1 (Branch) Coverage von Hand berechnen
  2. genau benennen, was als Statement zĂ€hlt – und was nicht (Kommentare, Docstrings, Leerzeilen)
  3. einen pytest-cov-Bericht lesen (Stmts, Miss, Branch, BrPart, Cover)
  4. erklĂ€ren, warum 100 % C0 ≠ 100 % C1 ist
  5. die Grenzen von Coverage richtig einordnen
Fahrplan (≈ 90 Minuten)
  • Warum Coverage? — 10 min
  • Testtheorie kompakt — 10 min
  • C0 berechnen — 25 min
  • C1 berechnen — 25 min
  • Subsumption — 5 min
  • pytest-cov & CI — 10 min
  • Grenzen & Zusammenfassung — 5 min

Roter Faden: Wir messen nicht nur Prozente – wir verstehen, wie sie zustande kommen.

3

Warum Coverage?

Was die CI sagt:

  • ✅ Ruff: Stil ist sauber
  • ✅ Pyright: Typen passen
  • ✅ pytest: alle Tests grĂŒn

Was die CI NICHT sagt:

  • ❓ Wurde jede Zeile ausgefĂŒhrt?
  • ❓ Wurde jeder Branch geprĂŒft?
  • ❓ Welcher Code ist ungetestet?

Menschen schÀtzen schlecht, wie viel sie getestet haben. Coverage macht es messbar.

Coverage = (vom Test ausgefĂŒhrte Elemente / alle Elemente) × 100 %

  • Eine Zahl statt BauchgefĂŒhl (z. B. 72 %, 88 %, 100 %)
  • Zeigt konkrete Zeilennummern, die ungetestet sind
  • Sagt, wo noch TestfĂ€lle fehlen

⚠ Coverage misst AusfĂŒhrung, nicht Korrektheit. Hohe Coverage ≠ fehlerfreier Code – dazu spĂ€ter mehr.

  • Programm P — der Code unter Test
  • Eingaberaum D — alle möglichen Eingaben (meist unendlich)
  • Testsuite T — endliche Teilmenge T ⊆ D, die wir wirklich ausfĂŒhren
  • Modell M(P) — Abstraktion des Programms (z. B. Kontrollflussgraph)
  • Kriterium C — Regel, was „ausreichend getestet" heißt

Kernidee: Coverage ist immer Coverage eines Modells. Anderes Modell → anderes Kriterium.

Ein CFG ist ein gerichteter Graph G = (V, E):

  • Knoten (V) = Anweisungen / Entscheidungen
  • Kanten (E) = möglicher Kontrollfluss

C0 zÀhlt Knoten, C1 zÀhlt Kanten aus Entscheidungen.

def classify(x):
    if x > 0:
        return "pos"
    elif x < 0:
        return "neg"
    return "zero"
3

Statement Coverage (C0)

Statement Coverage (C0): Jede ausfĂŒhrbare Anweisung mindestens einmal ausfĂŒhren.

C0 = (ausgefĂŒhrte Statements / alle Statements) × 100 %

  • Modell: CFG, Knoten = Statements
  • SchwĂ€chstes strukturelles Kriterium – nur die Basis
  • Aber: Was genau ist „eine Anweisung"? Genau hier irren sich die meisten ⟶
Was zÀhlt als Statement?

ZĂ€hlt NICHT:

  • ✋ Kommentare (inline und Block)
  • ✋ Leerzeilen
  • ✋ Docstrings (Modul, Funktion, Klasse)
  • ✋ Fortsetzungszeilen mehrzeiliger Statements

ZĂ€hlt:

  • ▾ import-Zeilen
  • ▾ Zuweisungen / return / Aufrufe
  • ▾ def- und class-Zeilen
  • ▾ Bedingungen (if, while, 
)

Alle Zahlen auf den nÀchsten Folien stammen aus echten coverage.py-LÀufen (code_examples/coverage/).

Beweis 1: Kommentare & Docstrings zÀhlen nicht
 1  """Modul-Docstring."""              ·  kein Statement
 7  import math  # Inline-Kommentar     ▾  zĂ€hlt (wegen import)
10  # Block-Kommentar                   ·  kein Statement
22  def addiere(a, b=2):                ▾  zĂ€hlt
26      """Funktions-Docstring."""      ·  kein Statement
27      # Kommentar im Körper           ·  kein Statement
28      summe = a + b                   ▾  zĂ€hlt

Legende: ▾ = Statement (zĂ€hlt fĂŒr C0) · · = kein Statement

Ein Inline-Kommentar macht eine Zeile nicht „zĂ€hlbar" – der Code davor tut es.

Beweis 2: Signaturen laufen beim Import
17  def deko(fn):              ▾ zĂ€hlt – „covered" schon beim Import
42  def nie_aufgerufen():      ▾ zĂ€hlt – „covered" schon beim Import
43      """..."""              · kein Statement
44      wert = 1               ✗ FEHLT  (Funktion nie aufgerufen)
45      return wert            ✗ FEHLT
  • Die def-/class-Zeile wird beim Definieren ausgefĂŒhrt, nicht beim Aufruf.
  • nie_aufgerufen wird nie getestet – trotzdem ist die def-Zeile abgedeckt, nur der Körper fehlt.
Beweis 3: Mehrzeilig = ein Statement
22  def addiere(        ▾ 1 Statement (die ganze Signatur)
23      a,              · gehört zu Zeile 22
24      b=2,            ·
25  ):                  ·
48  CONFIG = {          ▾ 1 Statement (das ganze Literal)
49      "a": 1,         ·
50      "b": 2,         ·
51  }                   ·

Ergebnis was_zaehlt.py: 16 Statements, 14 ausgefĂŒhrt, 2 fehlen
→ C0 = 14 / 16 = 88 %

✋ Mini-Übung: Wie viele Statements?
"""BegrĂŒĂŸung."""        # Zeile 1

def gruss(name):        # Zeile 3
    # baue die Nachricht
    text = f"Hallo {name}"
    return text         # Zeile 6

Frage an Sie: Wie viele ausfĂŒhrbare Statements zĂ€hlt coverage.py hier?

→ Lösung: 3 — die Zeilen def gruss, text = 
, return text. Docstring (1) und Kommentar zĂ€hlen nicht.

def note(score):
    """Doc."""
    # Schutzklausel
    if score < 0:
        return "ungĂŒltig"
    if score >= 90:
        return "A"
    return "B"

Tests: note(95)→"A", note(80)→"B"

Name     Stmts Miss Cover Missing
grade.py     6    1   83%   8
  • 6 Statements, Zeile 8 nie ausgefĂŒhrt
  • C0 = 5 / 6 = 83 %
  • Missing 8 = return "ungĂŒltig" (kein Test mit score < 0)
3

Branch Coverage (C1)

Branch Coverage (C1): Jedes Entscheidungsergebnis (True und False) mindestens einmal.

C1 = (ausgefĂŒhrte Branches / alle Branches) × 100 %

  • Modell: CFG, Kanten aus Entscheidungen
  • Jede einfache Entscheidung (if/while) hat 2 Branches (True/False)
Start
↓
x > 0 ?
True →
"pos"
False ↓
x < 0 ?
True →
"neg"
False ↓
"zero"
C1 berechnen: grade.py
Start
↓
score < 0 ?
True →
"ungĂŒltig"
False ↓
score ≄ 90 ?
True →
"A"
False ↓
"B"
abgedecktfehlt
Entscheidung True False
score < 0 ✗ ✓
score >= 90 ✓ ✓
  • 4 Branches, 3 abgedeckt
  • C1 = 3 / 4 = 75 %
  • Nur die True-Kante von score < 0 fehlt (kein Test mit negativem Wert)
def verarbeite(daten, pruefen=True):
    if pruefen:
        daten = daten.strip()
    return daten.upper()

Test: nur verarbeite(" hi ") (Default pruefen=True)

  • Alle 4 Statements laufen → C0 = 100 %
  • if pruefen nur als True getestet → False-Kante fehlt
  • C1 = 1 / 2 = 50 %
  • pytest-cov: Missing 5->7

Merke: Volle Statement-Coverage kann ganze Branches ĂŒbersehen. Deshalb ist C1 stĂ€rker.

✋ Mini-Übung: Berechnen Sie C0 und C1
def rabatt(betrag, mitglied):
    if betrag <= 0:
        return 0
    if mitglied:
        return betrag * 0.9
    return betrag

Test: nur rabatt(100, True)

→ Lösung: 6 Statements (def, 2× if, 3× return). AusgefĂŒhrt: 4 — def, beide if, return betrag * 0.9. Nicht ausgefĂŒhrt: return 0, return betrag.
C0 = 4 / 6 = 67 % · C1 = 2 / 4 = 50 % (je nur eine Kante pro if).

C1 subsumiert C0:
100 % C1 âŸč 100 % C0 (automatisch).

Beweis-Idee: Jede Kante (v₁ → v₂) fĂŒhrt beide Knoten aus. Alle Kanten abgedeckt âŸč alle Knoten abgedeckt.

Umkehrung gilt NICHT (siehe vorige Folie).

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

Praxis: C1 statt C0+C1 messen. Ziel im Unit-Test: 70–80 %+ C1.

3

pytest-cov in der Praxis

# Statement-Coverage + fehlende Zeilen
uv run pytest --cov=. --cov-report=term-missing

# zusÀtzlich Branch-Coverage (C1)
uv run pytest --cov=. --cov-branch --cov-report=term-missing
  • --cov=<paket> — was gemessen wird
  • --cov-report=term-missing — zeigt fehlende Zeilennummern
  • --cov-branch — schaltet Branch-Coverage ein
Name     Stmts Miss Branch BrPart Cover Missing
grade.py     6    1     4      1   80%   8
  • Stmts 6 · Miss 1 (Zeile 8)
  • Branch 4 · BrPart 1 (teilweise: nur eine Kante)
  • Cover 80 % ist kombiniert:
    (5 Stmts + 3 Branches) / (6 + 4) = 8/10
  • Nicht reines C0 (83 %) oder C1 (75 %)!
- name: Tests mit Coverage
  run: uv run pytest --cov=. --cov-report=term-missing --cov-fail-under=70
  • --cov-fail-under=70 lĂ€sst die CI fehlschlagen, wenn Coverage < 70 %
  • Verhindert, dass die Coverage unbemerkt sinkt
  • Richtwerte: 70 % Minimum · 80 % gut · 90 % sehr gut · 100 % selten sinnvoll
  1. Alle Entscheidungspunkte auflisten (if, while, for, 
)
  2. Pro Entscheidung beide Ergebnisse erzeugen (True und False)
  3. Schleifen: 0 / 1 / mehrere DurchlÀufe
  4. Alle RĂŒckgabewege (frĂŒhe Returns, Exceptions)
  5. Coverage erneut messen und prĂŒfen

RĂŒckwĂ€rts denken: „Welche Eingabe macht diese Bedingung True? Welche False?"

def teile(a, b):
    return a / b          # Bug: b == 0 nicht behandelt

def test_teile():
    assert teile(10, 2) is not None   # schwache Assertion
  • 100 % Coverage – und der Bug bleibt: schwache Assertion, b=0 ungetestet
  • Coverage zeigt LĂŒcken, garantiert aber keine Korrektheit
  • ErgĂ€nzen mit Äquivalenzklassen & Grenzwerten
Zusammenfassung
  • C0 = Statements ausgefĂŒhrt
  • C1 = Branch-Ergebnisse (T/F) ausgefĂŒhrt
  • Kommentare, Leerzeilen, Docstrings zĂ€hlen nicht
  • def/class zĂ€hlen & laufen beim Import
  • mehrzeilig = ein Statement
  • C1 subsumiert C0; 100 % C0 ≠ 100 % C1
  • --cov-branch: „Cover" ist kombiniert
  • 100 % Coverage ≠ fehlerfrei

Fragen?

DANKE

NĂ€chstes Kapitel: Anforderungen aus Tests