flowchart TB
E["E2E: ganze App wie ein Nutzer. Wenige, langsam, anfällig"]
I["Integration: mehrere Teile echt, z. B. Code plus Datenbank. Mittel viele"]
U["Unit: eine Funktion oder Klasse isoliert. Viele, sehr schnell"]
E --> I --> U
Tests und Qualität: Testpyramide, Property-based Testing, Mutation Testing und Reviews von KI-Code
Track Konzepte · Qualität und Arbeit mit KI · ca. 70 Min.
Worum es geht
Eine KI schreibt dir ein Modul und gleich 40 Tests dazu. Alle sind grün, die Abdeckung (coverage) steht bei 100 Prozent. Du schaust kurz drüber, es sieht plausibel aus, du mergst. Zwei Wochen später stimmt ein Betrag auf einer Rechnung nicht. Was ist schiefgegangen? Die Tests haben jede Zeile ausgeführt, aber kaum etwas geprüft. Grün heißt nur: “Die Tests, die jemand geschrieben hat, finden keinen Fehler.” Es heißt nicht: “Es gibt keinen Fehler.”
Diese Lektion gibt dir Werkzeuge, mit denen du diese Lücke systematisch schließt. Du ordnest Tests in der Testpyramide (test pyramid) ein und lernst, wo welche Testart ihr Geld wert ist. Du schreibst einen Property-based Test (Eigenschaftstest), der einen versteckten Fehler findet, und du nutzt Mutation Testing, um zu messen, ob Tests wirklich etwas prüfen. Du machst einen flaky (unzuverlässigen) Test deterministisch. Und du reviewst eine KI-Funktion, die plausibel aussieht und drei Fehler enthält.
Alles läuft im Browser. Weil dort weder pytest noch hypothesis laufen, bauen wir zwei kleine Werkzeuge selbst: einen Property-Test-Runner (20 Zeilen) und einen Mini-Mutator mit dem Modul ast. So siehst du, wie die großen Werkzeuge im Kern arbeiten.
Die Lektion beantwortet den Prüfstein: Welche Testarten ergänzt du, wenn eine KI große Teile des Codes schreibt, und warum?
Plane ehrlich 70 Minuten ein: etwa 25 Minuten Lesen, 45 Minuten Übungen (fünf Stück).
Von JS/TS her gedacht
Du kennst die Werkzeuge, nur die Namen sind andere:
| Konzept | JS/TS | Python (hier) |
|---|---|---|
| Unit- und Integrationstest | Jest, Vitest (test, expect) |
pytest, hier: eigene Check-Funktionen |
| Mock, Fake | jest.fn(), vi.mock |
Fake-Klasse oder Funktion als Argument |
| E2E-Test | Playwright, Cypress | Playwright (gleiche Idee, anderer Client) |
| Property-based Testing | fast-check |
hypothesis (hier: eigener Mini-Runner) |
| Mutation Testing | Stryker | mutmut (hier: eigener Mini-Mutator) |
| Zeit einfrieren | jest.useFakeTimers(), vi.setSystemTime |
Uhr als Argument übergeben |
Property-based Testing in reinem JavaScript, mit Node ausgeführt. Ein Zufallsgenerator mit festem Seed (Startwert), damit der Lauf wiederholbar ist, und eine Funktion, die Text kürzen soll:
function mulberry32(a) {
return () => {
a |= 0; a = (a + 0x6D2B79F5) | 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
const kuerze = (text, max) => text.length <= max ? text : text.slice(0, max - 3) + "...";
function pruefe(eigenschaft, erzeuger, durchlaeufe = 200, seed = 0) {
const zufall = mulberry32(seed);
for (let i = 0; i < durchlaeufe; i++) {
const eingabe = erzeuger(zufall);
if (!eigenschaft(...eingabe)) return { lauf: i, eingabe };
}
return null;
}
const ganz = (z, a, b) => a + Math.floor(z() * (b - a + 1));
const erzeuger = (z) => ["ab ".repeat(6).slice(0, ganz(z, 0, 12)), ganz(z, 0, 12)];
console.log(kuerze("Hallo Welt", 8));
console.log(pruefe((t, m) => kuerze(t, m).length <= m, erzeuger, 200, 0));Ausgabe:
Hallo...
{ lauf: 0, eingabe: [ 'ab ', 0 ] }
Das Beispiel kuerze("Hallo Welt", 8) sieht richtig aus. Der Zufallstest findet sofort einen Fall, den du nicht bedacht hast: Wenn die Grenze kleiner als 3 ist, wird das Ergebnis länger als erlaubt. Dieselbe Idee bauen wir gleich in Python.
Konzept
Schritt 1: Testpyramide und Testdoubles
Tests haben drei Eigenschaften, die gegeneinander tauschen (Trade-off): Tempo, Stabilität und Aussagekraft (wie realistisch ist der Test). Die Testpyramide sortiert Tests nach diesem Tausch:
| Ebene | Prüft | Tempo | Stabilität | Fehlerort finden |
|---|---|---|---|---|
| Unit | Logik einer Einheit | sehr schnell | hoch | leicht, der Test zeigt auf die Funktion |
| Integration | Zusammenspiel, z. B. SQL gegen echte Datenbank | mittel | mittel | mittel |
| E2E | ganzer Weg durch das System | langsam | niedrig (Netzwerk, Timing, Browser) | schwer (“Seite zeigt falschen Preis”) |
Die Form der Pyramide ist eine Faustregel: Je weiter unten ein Test liegt, desto billiger ist er und desto genauer zeigt er auf den Fehler. Je weiter oben, desto näher an der Realität, aber desto teurer und schwerer zu debuggen. Viele E2E-Tests und wenige Unit-Tests nennt man darum scherzhaft Eistüte (ice cream cone), und sie ist ein bekanntes Warnsignal.
Testdoubles (Ersatzobjekte) ersetzen Abhängigkeiten, die langsam oder unberechenbar sind. Der Fake hat eine funktionierende, vereinfachte Implementierung (ein Dict statt Datenbank, siehe Lektion 10). Der Stub liefert feste Antworten. Der Mock prüft zusätzlich, ob er richtig aufgerufen wurde. Vorsicht beim Mocken: Wer jede interne Funktion mockt, testet am Ende nur noch, dass der Code so aufgebaut ist, wie er aufgebaut ist. Ändert sich die Struktur, brechen die Tests, obwohl das Verhalten gleich bleibt.
Drei Ergänzungen für Fortgeschrittene, die du einordnen können solltest:
- Contract Tests (Vertragstests): prüfen, dass zwei Teams oder Dienste sich an die gemeinsame Schnittstelle halten. Beim consumer-driven Ansatz schreibt der Aufrufer auf, was er braucht (Lektion 13).
- Fuzzing: ein Programm füttert deinen Code mit massenhaft zufälligen oder zerstückelten Eingaben und sucht Abstürze. Es ist eng verwandt mit Property-based Testing, aber ohne eine fachliche Eigenschaft, nur mit “darf nicht abstürzen”.
- Flaky Tests: Tests, die ohne Codeänderung mal grün, mal rot sind. Sie zerstören das Vertrauen in die ganze Suite (Schritt 4).
Schritt 2: Property-based Testing
Ein normaler Test prüft Beispiele: Eingabe A ergibt Ausgabe B. Ein Property-based Test (Eigenschaftstest) prüft eine Regel, die für alle Eingaben gelten muss, und lässt den Computer viele Eingaben erzeugen. Statt “kuerze(‘Hallo Welt’, 8) ist ‘Hallo…’” schreibst du: “Das Ergebnis ist nie länger als die Grenze.”
Der Runner, den wir brauchen, ist klein. Er bekommt die Eigenschaft (eine Funktion, die True oder False liefert), einen Erzeuger für Eingaben, die Anzahl der Durchläufe und einen Seed. Mit gleichem Seed erzeugt er dieselben Eingaben, also ist jeder Fund reproduzierbar. Er gibt das erste Gegenbeispiel (counterexample) zurück oder None:
Jetzt eine KI-Funktion, die Text kürzen soll, und ein Beispieltest, der sie freispricht:
Ausgabe: Hallo... kurz abc.... Alle drei Beispiele stimmen. Jetzt die Eigenschaft “nie länger als die Grenze”. Der Erzeuger baut zufällige Texte aus den Zeichen a, b und Leerzeichen mit Länge 0 bis 12 und eine Grenze von 0 bis 12:
Ausgabe:
(9, (' b ', 1), 'verletzt')
(9, (' b ', 1), 'verletzt')
(4, (' ab', 0), 'verletzt')
Drei Dinge fallen auf. Erstens: Der Test findet einen Fehler, den die Beispiele nicht fanden. Bei Grenze 1 liefert text[:-2] fast den ganzen Text plus drei Punkte. Zweitens: Gleicher Seed, gleiches Ergebnis (erste und zweite Zeile). Drittens: Anderer Seed, anderes Gegenbeispiel, aber derselbe Fehler. Der Seed ist dein Schlüssel zum Reproduzieren: Wenn ein Test in der CI rot wird, schreibst du den Seed ins Log, sonst findest du den Fehler nie wieder.
Echte Bibliotheken (hypothesis, fast-check) können zusätzlich shrinken (verkleinern): Sie suchen nach dem kleinsten Gegenbeispiel, hier etwa ("a", 0). Das macht Fehlermeldungen lesbar. Dein Mini-Runner tut das nicht, du siehst das erste Beispiel, wie es kommt.
Die schwierige Kunst bei Property-Tests ist nicht der Runner, sondern die Eigenschaft. Gute Eigenschaften sind zum Beispiel: Rundreise (dekodiere(kodiere(x)) == x), Invarianten (die Summe der Teile ist das Ganze), Ordnung (Ergebnis ist sortiert und hat dieselben Elemente) und Vergleich mit einer einfachen, langsamen Referenzlösung. Eine zu schwache Eigenschaft (“Ergebnis ist ein Text”) findet nichts, genau wie ein zu schwacher Beispieltest.
Schritt 3: Mutation Testing
Property-Tests suchen Fehler im Code. Mutation Testing sucht Schwächen in den Tests. Die Idee: Du baust absichtlich kleine Fehler in den Code ein (jeweils einen), die Mutanten (mutants). Dann lässt du die Tests laufen. Wenn ein Test rot wird, ist der Mutant getötet (killed): Die Tests bemerken den Fehler. Wenn alle Tests grün bleiben, überlebt der Mutant (survived): Ein Fehler wäre unbemerkt in die Produktion gegangen. Die Quote der getöteten Mutanten heißt Mutation Score.
Typische Mutationen: + zu -, < zu <=, == zu !=, and zu or. Unser Mini-Mutator liest den Code mit ast (abstrakter Syntaxbaum, abstract syntax tree), findet solche Operatoren und erzeugt pro Fundstelle eine Kopie mit genau einem getauschten Operator:
So liest du den Mutator: ast.parse macht aus dem Quelltext einen Baum, ast.walk besucht alle Knoten, ast.unparse macht aus dem geänderten Baum wieder Quelltext. Für jede Stelle mit einem Operator aus TAUSCH entsteht ein Mutant, in dem genau dieser Operator durch seinen “Gegenspieler” ersetzt ist. Die Reihenfolge der Mutanten folgt der Reihenfolge, in der ast.walk den Baum besucht, nicht den Zeilennummern. Darum steht in der Ausgabe unten Zeile 4 vor Zeile 3.
Dazu ein kleiner Prüfer: besteht führt einen Quelltext aus und lässt eine Liste von Testfällen ((Argumente), erwartet) darüber laufen. Eine Ausnahme zählt als “Test rot”. ueberlebende gibt die Mutanten zurück, die alle Tests überstehen:
Jetzt ein Beispiel. Eine Rabattfunktion: Ab mehr als 10 Stück gibt es 10 Prozent. Zwei Testfälle, die beide Zweige der Funktion ausführen:
Ausgabe:
Original besteht: True
Zeile 2: > -> >= -> ÜBERLEBT
Zeile 4: * -> / -> getötet
Zeile 3: * -> / -> getötet
Zeile 3: * -> / -> getötet
Die Tests führen jede Zeile aus, trotzdem überlebt ein Mutant: menge >= 10 statt menge > 10. Kein Test prüft genau die Grenze 10. Ein Mensch oder eine KI, die an dieser Stelle versehentlich >= schreibt, würde nicht erwischt. Das ist die Lücke, die Zeilenabdeckung nie zeigt. Der Fix ist ein dritter Testfall, ((10, 3.0), 30.0): Bei genau 10 Stück gibt es keinen Rabatt. Dann überlebt kein Mutant mehr (ausprobiert).
Zwei Einschränkungen gehören zur Ehrlichkeit. Erstens kann es äquivalente Mutanten geben: Der Mutant verhält sich genauso wie das Original (etwa wenn eine Grenze für das Ergebnis egal ist), dann kann kein Test ihn töten. Zweitens ist Mutation Testing teuer: Für jeden Mutanten laufen die Tests (bei echten Werkzeugen oft Hunderte bis Tausende Mutanten). Darum nutzt man es gezielt, zum Beispiel für kritische Module und für von KI geschriebene Tests, nicht für das ganze Projekt bei jedem Commit.
Schritt 4: Characterization Tests und Flaky Tests
Characterization Tests (Charakterisierungstests) brauchst du bei Legacy-Code, den du ändern sollst, aber nicht verstehst. Du fragst nicht, was die Funktion tun sollte, sondern hältst fest, was sie tatsächlich tut, auch wenn es ein Fehler ist. So hast du ein Sicherheitsnetz für Refactoring: Ändert sich das Verhalten, schlägt ein Test an, und du entscheidest bewusst, ob das gewollt ist. Das Verfahren: Funktion mit vielen Eingaben aufrufen, Ausgaben (und Fehler) aufschreiben, als Erwartung einfrieren. Man nennt es auch Golden Master.
Ausgabe:
((1000, 1), 50)
((1000, 6), 80)
((100000, 1), 5000)
((100001, 6), 10000)
((-1000, 1), -50)
((999.99, 1), 49)
(('abc', 1), 'TypeError')
((1000, None), 'TypeError')
Du siehst zwei Verhaltensweisen, die vermutlich nie jemand bewusst entschieden hat: Negative Umsätze ergeben negative Boni, und int(...) schneidet ab (49 statt 49,99). Ein Characterization Test friert beides ein. Ob es ein Fehler ist, entscheidest du später, mit dem Fachbereich, und änderst dann Test und Code gemeinsam. Das gleiche Muster brauchst du bei KI-Code ohne Spezifikation.
Flaky Tests sind meist kein Pech, sondern versteckte Eingaben: der Test liest Zeit, Zufall, Reihenfolge, Netzwerk oder geteilten Zustand, den er nicht kontrolliert. Hier ein Test, der auf Zufall wartet, und 100 Läufe mit jeweils anderem Seed:
Ausgabe: 26 von 100 Läufen rot. Der Code ist nicht kaputt, der Test ist es. Die Reparatur heißt Injektion: Zufall und Zeit werden von außen hineingegeben, wie bei der Dependency Injection aus Lektion 10. Im Test übergibst du eine kontrollierte Version:
Ausgabe: ['Niete', 'Niete', 'Hauptpreis']. Jetzt bestimmt der Test, was “zufällig” kommt. Für die Zeit ist es dasselbe Muster mit einer Uhr als Funktion:
Ausgabe: True und danach False. Der Test “springt” auf die Sekunde genau über die Grenze, ohne zu warten. In Produktion gibst du time.time als Uhr hinein. Genau das machst du in Übung 3.
Schritt 5: Codequalität in Kürze
Qualität hat Messgrößen, die du kennen solltest, aber nie blind anwenden. Alles in dieser Tabelle ist allgemeines Fachwissen, ohne Zahlenwerte:
| Begriff | Bedeutung | Praxis |
|---|---|---|
| Cognitive Complexity | Wie schwer ist eine Funktion zu verstehen? Tiefe Verschachtelung zählt stärker als viele Zeilen. | Früh abbrechen (guard clauses), Teile in benannte Funktionen auslagern |
| Kopplungsmetriken | Wie viele andere Module kennt ein Modul? | Viele Abhängigkeiten sind ein Signal für Probleme (Lektion 10) |
| Code Smells | Warnzeichen im Code (lange Funktion, Duplikate, Shotgun Surgery) | Hinweis, kein Beweis |
| Technische Schuld (technical debt) | Bewusst oder unbewusst gewählte Abkürzung, die später Zinsen kostet | Sichtbar machen, bewusst aufnehmen, geplant zurückzahlen |
| Refactoring-Strategien | Struktur ändern, ohne Verhalten zu ändern | Erst Tests (notfalls Characterization Tests), dann kleine Schritte |
Zur Delivery-Praxis nur drei Stichworte: Trunk-based Development (kleine Änderungen oft in den Hauptzweig, das geht nur mit schnellen, verlässlichen Tests), DORA-Metriken (Messgrößen für Liefer- und Betriebsqualität, die genaue Liste bitte prüfen), RFCs und ADRs (Entscheidungen schriftlich, Lektion 11). Dein Testsystem ist die Voraussetzung dafür, dass man oft und sicher ausliefern kann.
Schritt 6: KI-Code prüfen
Wenn eine KI Code schreibt, ändert sich das Risiko, nicht die Regeln. Die KI schreibt schnell, sauber formatiert und überzeugt klingend. Typische Fehlermuster, die du kennen solltest (alle bis auf Off-by-one stehen so in der Quelle):
- Halluzinierte APIs und Pakete: Methoden, die es nicht gibt, oder Paketnamen, die erfunden sind (Lieferketten-Risiko, wenn jemand den Namen später real belegt).
- Fehlende Fehlerpfade: Der Happy Path ist da, kaputte Eingaben, Timeouts und leere Listen fehlen.
- Plausibel wirkende Nebenläufigkeit: Code mit Threads oder async, der im Test läuft und unter Last Race Conditions hat (Lektion 1).
- Off-by-one und Randfälle: Grenzen, die um eins danebenliegen.
- Overengineering, duplizierte Logik, veraltete Muster.
Die Antwort ist nicht “der KI mehr misstrauen”, sondern Leitplanken (guardrails) bauen, die automatisch prüfen: Typen, Linter, Architekturtests (Fitness Functions), Contract Tests und CI. Dazu staffelst du die Review-Tiefe nach Blast Radius (Schadensradius): Auth, Migrationen, Geldflüsse und Nebenläufigkeit zuerst und besonders genau. Ein Tippfehler im Button-Text ist etwas anderes als ein falscher Rundungsfehler in einer Rechnung.
Eine Review-Checkliste für KI-Code (allgemeines Fachwissen, so formuliert, dass du sie abhaken kannst):
- Gibt es das wirklich? Jede Methode, jedes Modul, jeden Parameter gegen die Doku prüfen oder ausführen. Besonders Namen, die “wie aus einer anderen Sprache” klingen.
- Was passiert bei kaputten Eingaben? Leer,
None, falsches Format, Netzwerk weg. Steht der Fehlerpfad in der Beschreibung? - Stimmen die Grenzen? Erstes, letztes, genau auf der Grenze, mehr als vorhanden, null.
- Wer teilt Zustand? Globale Variablen, Threads, Caches, Zeit, Zufall.
- Prüfen die Tests wirklich etwas? Mutation Testing, Property-Tests für Invarianten, ein Test pro Fehlerpfad.
- Wie groß ist der Schaden, wenn es falsch ist? Danach richtet sich, wie tief du schaust.
Fazit für den Prüfstein: Du ergänzt Tests, die nicht von derselben KI aus derselben Annahme stammen. Property-Tests für Invarianten, die du aufschreibst. Mutation Testing, das misst, ob die mitgelieferten Tests etwas festnageln. Characterization Tests, wenn du bestehendes Verhalten schützen willst. Contract Tests an den Schnittstellen. Wenige E2E-Tests auf kritischen Pfaden. Und Leitplanken in der CI, die jeden KI-Vorschlag automatisch gegenprüfen.
Falle
- Abdeckung mit Qualität verwechseln. 100 Prozent Zeilenabdeckung heißt “ausgeführt”, nicht “geprüft”. Ein Test ohne
asserterhöht die Abdeckung genauso (Schritt 3). - Zufallstest ohne Seed. Wenn der Fehler nicht reproduzierbar ist, ist das Gegenbeispiel wertlos. Seed immer protokollieren (Schritt 2).
- Eigenschaft zu schwach. “Die Liste hat die richtige Länge” lässt falsche Inhalte durch. Eine gute Eigenschaft ist so streng wie der Vertrag (Übung 1).
- Flaky Tests wiederholen statt reparieren. Ein automatischer Retry versteckt das Problem und macht die Suite langsamer. Zeit und Zufall hineingeben, nicht zudecken (Schritt 4).
- Alles mocken. Wer jede interne Funktion mockt, testet die Verdrahtung, nicht das Verhalten.
- KI-Tests als Beweis nehmen. Die KI hat Code und Tests mit derselben Annahme geschrieben. Wenn die Annahme falsch ist, sind beide falsch, und alles bleibt grün.
Übungen
Übung 1: Property-Test gegen einen versteckten Fehler (ca. 10 Min.)
Eine KI hat teile_auf(betrag_cent, n) geschrieben. Die Funktion verteilt einen Betrag in Cent auf n Personen. Der Vertrag: Das Ergebnis ist eine Liste mit n ganzen Zahlen, die Summe ergibt genau den Betrag, und kein Anteil unterscheidet sich vom anderen um mehr als 1 Cent. Die Funktion hat einen Fehler, den ein einzelner Beispieltest leicht übersieht.
Schreibe die Eigenschaft eigenschaft(f, betrag_cent, n). Sie bekommt die zu prüfende Funktion f, ruft sie auf und gibt True zurück, wenn der Vertrag eingehalten ist, sonst False. Du nutzt den Runner pruefe_eigenschaft aus Schritt 2 und den Erzeuger erzeuger_teilung(zufall), der Beträge von 0 bis 1000 Cent und 1 bis 7 Personen erzeugt. Die letzte Zeile der Zelle gibt eigenschaft zurück. Der Check prüft deine Eigenschaft gegen eine korrekte Version (sie darf nie anschlagen) und gegen mehrere fehlerhafte Versionen (sie muss anschlagen).
Übersetze den Vertrag Punkt für Punkt in Bedingungen: Wie viele Einträge, welche Summe, wie weit liegen größter und kleinster Anteil auseinander? Eine Eigenschaft, die nur einen Teil des Vertrags prüft, lässt Fehler durch, die gegen den anderen Teil verstoßen.
def teile_auf(betrag_cent, n):
"""Von der KI geschrieben, nicht ändern."""
anteil, rest = divmod(betrag_cent, n)
anteile = [anteil] * n
for i in range(rest - 1):
anteile[i] += 1
return anteile
def eigenschaft(f, betrag_cent, n):
anteile = f(betrag_cent, n)
return (len(anteile) == n
and sum(anteile) == betrag_cent
and max(anteile) - min(anteile) <= 1)
print(pruefe_eigenschaft(lambda b, n: eigenschaft(teile_auf, b, n), erzeuger_teilung, seed=1))
eigenschaftDer Fehler: range(rest - 1) verteilt einen Cent zu wenig. Bei jedem Rest ab 1 geht ein Cent verloren, die Summe stimmt nicht. Die Eigenschaft prüft alle drei Teile des Vertrags (Länge, Summe, Spreizung). Eine Eigenschaft nur mit Länge hätte den Fehler nie gefunden.
Übung 2: Mutanten töten (ca. 10 Min.)
Die Versandkosten eines Shops: Ab 50 Euro Warenwert und höchstens 10 kg ist der Versand kostenlos. Sonst kosten Pakete bis einschließlich 2 kg 3,90 Euro. Schwerere Pakete kosten 5,90 Euro plus 0,50 Euro für jedes Kilogramm über 2 kg.
Du hast die Funktion als Text QUELLE und eine Liste testfaelle mit einem ersten Fall. Ergänze Testfälle der Form ((gewicht_kg, warenwert), erwartete_kosten), bis alle Mutanten sterben. Mit ueberlebende(QUELLE, "versandkosten", testfaelle) aus Schritt 3 siehst du, welche noch überleben. Der Check lässt deine Testfälle gegen das Original laufen (sie müssen bestehen), zählt die getöteten Mutanten und erlaubt höchstens 12 Testfälle. Gib die Liste testfaelle als letzten Ausdruck zurück.
Jeder überlebende Mutant verrät eine Zeile und einen Operator. Frage dich: Welcher Eingabewert würde Original und Mutant unterschiedlich antworten lassen? Bei Vergleichen sind das fast immer die Werte genau auf der Grenze. Rechne die erwartete Antwort aus der Beschreibung oben aus, nicht durch Aufruf der Funktion.
QUELLE = '''def versandkosten(gewicht_kg, warenwert):
if warenwert >= 50 and gewicht_kg <= 10:
return 0.0
if gewicht_kg <= 2:
return 3.9
return 5.9 + (gewicht_kg - 2) * 0.5
'''
testfaelle = [
((1, 20), 3.9),
((2, 20), 3.9),
((4, 20), 6.9),
((1, 50), 0.0),
((10, 50), 0.0),
((12, 50), 10.9),
]
for b in ueberlebende(QUELLE, "versandkosten", testfaelle):
print("überlebt:", b)
testfaelleWarum diese Fälle? (2, 20) tötet <= zu < in der Gewichtsgrenze. (4, 20) prüft die Formel für schwere Pakete (+, -, *). (1, 50) tötet >= zu > bei der Wertgrenze. (10, 50) tötet <= zu < bei 10 kg. (12, 50) tötet and zu or: Bei or wäre der Versand wegen des hohen Werts fälschlich gratis.
Übung 3: Flaky Test reparieren durch Injektion (ca. 8 Min.)
Die Klasse Sitzung verwaltet eine Login-Sitzung. Sie liest Zeit und Zufall global. Ein Test für “nach 30 Sekunden abgelaufen” müsste echte 30 Sekunden warten, und ein Test für neues_token kann das Ergebnis nicht vorhersagen. Beides sind Flaky-Test-Ursachen.
Baue sie so um, dass Zeit und Zufall von außen kommen:
- Der Konstruktor heißt
Sitzung(ttl_sekunden, uhr=None, zufall=None). uhrist eine Funktion ohne Argumente, die die aktuelle Zeit in Sekunden liefert.zufallist ein Objekt mit einer Methodechoice, wierandom.Random(...).- Ohne Angabe (
None) gelten die echte Zeit (time.time) und das Modulrandom, damit sich die Produktion nicht ändert. - Der Startzeitpunkt wird im Konstruktor über die Uhr gelesen.
abgelaufen()istTrue, sobaldttl_sekundenSekunden oder mehr seit dem Start vergangen sind. neues_token()liefert 8 Zeichen aus"abcdef0123456789"und benutzt dafürzufall.choice.
Die letzte Zeile gibt die Klasse Sitzung zurück.
Wo steht im Konstruktor und in den Methoden ein direkter Zugriff auf die Außenwelt? Jede dieser Stellen braucht einen Namen, den der Aufrufer ersetzen kann. Und: Was passiert, wenn der Standardwert eines Parameters schon beim Definieren der Funktion ausgewertet wird?
import random, time
class Sitzung:
def __init__(self, ttl_sekunden, uhr=None, zufall=None):
self.ttl = ttl_sekunden
self.uhr = uhr if uhr is not None else time.time
self.zufall = zufall if zufall is not None else random
self.start = self.uhr()
def abgelaufen(self):
return self.uhr() - self.start >= self.ttl
def neues_token(self):
return "".join(self.zufall.choice("abcdef0123456789") for _ in range(8))
SitzungDie Uhr ist eine Funktion (uhr()), kein Wert. Ein Wert wäre nach dem Setzen eingefroren, die Funktion liefert bei jedem Aufruf die aktuelle Zeit. Im Test steuerst du sie, in Produktion bleibt time.time.
Übung 4: KI-Code reviewen (ca. 10 Min.)
Eine KI hat diese Funktion geschrieben. Sie sieht gepflegt aus, und ein kurzer Test mit einer sauberen CSV-Datei läuft durch. Die Beschreibung (Docstring) sagt: Umsatz pro Kunde aus einer Text-CSV mit den Spalten datum;kunde;betrag, die n umsatzstärksten Kunden absteigend, Beträge dürfen ein Dezimalkomma haben, unlesbare Zeilen werden übersprungen. Mit als_text=True kommt eine Tabelle als Text zurück.
Die Funktion enthält drei Fehler. Gehe die Review-Checkliste aus Schritt 6 durch (Gibt es die Methoden wirklich? Fehlerpfade? Grenzen?) und trage in markiert die Zeilennummern ein, in denen ein Fehler steckt. Bei einem Fehler, der über mehrere Zeilen geht, genügt eine Zeile aus dem betroffenen Bereich. Der Check bestimmt die Wahrheit selbst durch Ausführen: Er ersetzt jede Stelle probeweise durch eine korrigierte Version und prüft, ob sich dadurch Testergebnisse ändern. Echte Fehler verbessern das Ergebnis, harmlos aussehende Zeilen nicht. Du darfst die Funktion in der Zelle auch ausführen: exec(KI_CODE) legt top_kunden an.
Gehe die Beschreibung Satz für Satz durch und frage bei jedem Satz: Wo im Code wird das eingelöst? Spiele danach drei Eingaben im Kopf durch: eine kaputte CSV-Zeile, n=1 und als_text=True. Welche Zeile bricht jeweils, oder liefert etwas anderes als die Beschreibung verspricht?
markiert = {6, 9, 11}
markiertZeile 6 (und 7): Fehlender Fehlerpfad. Die Beschreibung verlangt, unlesbare Zeilen zu überspringen. Der Code bricht bei der ersten kaputten Zeile mit einer Exception ab (n/a als Betrag oder eine Zeile mit zu wenig Spalten). Es fehlt ein try und except ValueError: continue.
Zeile 9: Off-by-one. rang[:n - 1] liefert nur n - 1 Kunden, bei n=1 sogar keinen. Richtig ist rang[:n].
Zeile 11: Halluzinierte API. str.pad_left gibt es in Python nicht. In JavaScript heißt es padStart, in Python rjust. Der Fehler tritt nur mit als_text=True auf, darum fällt er in einem kurzen Test auf dem Standardpfad nicht auf.
Die Zeilen 5 und 8 sehen verdächtig aus (strip, splitlines, reverse=True), sind aber korrekt.
Übung 5: Welche Tests ergänzt du? (ca. 6 Min.)
Zwei Fälle mit Randbedingungen. Gib ein Tupel mit zwei Buchstaben zurück, zum Beispiel ("A", "B").
Fall 1. Eine KI hat das Modul preise.py geschrieben (reine Funktionen, Geldbeträge, viele Randfälle) und gleich 40 Unit-Tests dazu. Alle sind grün, die Zeilenabdeckung liegt bei 100 Prozent. Das Modul läuft in Sekunden durch. Ihr wollt wissen, ob diese Tests das Verhalten wirklich festnageln. Welche Ergänzung gibt darüber die beste Auskunft?
- A. Einen E2E-Test durch den ganzen Bestellvorgang im Browser ergänzen, der jede Nacht läuft.
- B. Mutation Testing über das Modul laufen lassen und überlebende Mutanten mit neuen Tests töten.
- C. Die Zeilenabdeckung auf Branch Coverage mit 100 Prozent umstellen und die Tests dann einfrieren.
- D. Alle Aufrufe zwischen den Funktionen mocken, damit jeder Test genau eine Funktion prüft.
Fall 2. Ein Team hat 60 E2E-Tests im Browser als einzige Tests. Der Lauf dauert 45 Minuten. Jede Woche sind zwei bis drei davon ohne Codeänderung rot (flaky), und bei einem echten Fehler meldet der Test nur “Seite zeigt falschen Preis”, die Suche dauert lange. Die Preisregeln sind reine Funktionen, die Datenbankabfragen haben eigene Zugriffsfunktionen. Das Ziel ist schnelleres Feedback und leichtere Fehlerlokalisierung, ohne die Sicherheit für den Bezahlvorgang aufzugeben. Was machst du?
- A. Preisregeln als Unit-Tests, Abfragen als Integrationstests, E2E auf wenige kritische Pfade kürzen.
- B. Die E2E-Tests auf mehr Maschinen parallel laufen lassen und rote Tests automatisch dreimal wiederholen.
- C. Alle 60 E2E-Tests durch Unit-Tests mit Mocks ersetzen, auch den Bezahlvorgang über mehrere Dienste.
- D. Die E2E-Tests behalten und ein Dashboard bauen, das die flaky Tests nach Häufigkeit sortiert anzeigt.
Fall 1: Was misst Abdeckung, und was misst sie nicht? Fall 2: Wo auf der Pyramide liegt Fehlerlokalisierung am billigsten, und welcher Preis steckt in der Antwort, die alles dorthin verschiebt?
antwort = ("B", "A")
antwortFall 1, B: Mutation Testing misst, ob die Tests Änderungen im Verhalten bemerken. Überlebende Mutanten zeigen genau die Stellen, an denen ein Fehler unentdeckt bliebe. Fall 2, A: Die Pyramide ordnen: viel Logik unten (schnell, genau), Datenbankzugriff in der Mitte, oben nur wenige E2E-Tests für den Bezahlvorgang, bei dem das Zusammenspiel zählt.
Merksatz
Grün heißt nur “die vorhandenen Tests finden nichts”: Prüfe mit Property-Tests und Mutation Testing, ob Tests wirklich etwas festnageln, halte die Pyramide breit unten, mache Zeit und Zufall zu Eingaben, und reviewe KI-Code nach Blast Radius mit einer festen Checkliste.
Prüfstein
- Welche Testarten ergänzt du, wenn eine KI große Teile des Codes schreibt, und warum? Nenne mindestens drei und sage jeweils, welchen Fehler sie finden, den die anderen nicht finden.
- Ein Test ist ohne Codeänderung mal grün, mal rot. Nenne drei typische Ursachen und beschreibe, wie du eine davon beseitigst, ohne den Test zu wiederholen.
Quelle: quellen/konzeptuebersicht-software-grundlagen.md, Abschnitt “8. Security und Qualität” (Tests: Testpyramide, Unit/Integration/E2E, Mocks, Property-based Testing; Wartbarkeit: Refactoring, Code Smells, technische Schuld, Code Review) und “Querschnitt für die Arbeit mit KI” (Testbarkeit einfordern); quellen/konzeptuebersicht-software-fortgeschritten.docx, Schicht 8 (Testen vertieft: Contract Tests, Property-based Testing, Mutation Testing, Fuzzing, Characterization Tests, Flaky Tests; Codequalität: Cognitive Complexity, Kopplungsmetriken, Refactoring-Strategien, technische Schuld; Delivery-Praxis: Trunk-based Development, DORA-Metriken, RFCs und ADRs; Prüfstein “Welche Testarten ergänzt du, wenn eine KI große Teile des Codes schreibt, und warum?”) und Querschnitt “Arbeit mit KI” (Verträge statt Prompts, Leitplanken bauen, Blast Radius einschätzen, typische KI-Fehlermuster erkennen, KI-Code gezielt reviewen).
Über die Quelle hinaus (allgemeines Fachwissen): die Erklärung der Testpyramide und der Eistüte, die Unterscheidung von Fake, Stub und Mock, die Definitionen von Mutation Score, getöteten und überlebenden Mutanten und äquivalenten Mutanten, der Hinweis auf Shrinking in hypothesis und fast-check, die Namen der Werkzeuge (Jest, Vitest, Playwright, hypothesis, fast-check, mutmut, Stryker), die Erklärung von Golden Master, die Ursachen flaky Tests, die Review-Checkliste, die Aufnahme von Off-by-one und Randfällen in die Liste der KI-Fehlermuster, die Cognitive-Complexity-Beschreibung ohne Zahlen. Die Zeilen “26 von 100 Läufen rot”, die Gegenbeispiele der Property-Tests, die Mutantenlisten und die Bonus-Ausgaben stammen aus dem Ausführen des Codes mit Python 3 beziehungsweise Node. Die genaue Liste der DORA-Metriken: bitte prüfen.