Claude Code Prompt zur Generierung von Grenzfall-Tests
Lerne einen Claude Code Prompt zur Testgenerierung, der hochwertige Tests erzeugt, indem er Grenzen, Invarianten und Fehlerarten statt nur Happy Paths anvisiert.

Warum Happy-Path-Testgenerierung Zeit verschwendet
Auto-generierte Test-Suites sehen oft beeindruckend aus: Dutzende Tests, viel Setup-Code, und jeder Funktionsname taucht irgendwo auf. Viele dieser Tests sind aber nur „es funktioniert, wenn alles normal ist“-Überprüfungen. Sie bestehen leicht, fangen selten Bugs ein und verursachen trotzdem Aufwand beim Lesen und Warten.
Bei einem typischen Claude Code Test-Generierungs-Prompt neigt das Modell dazu, die Beispiel-Inputs zu spiegeln, die es sieht. Man erhält Variationen, die anders aussehen, aber dasselbe Verhalten abdecken. Das Ergebnis ist eine große Suite mit dünner Abdeckung an den relevanten Stellen.
High-Signal-Tests sind anders. Es ist die kleine Menge, die den Vorfall vom letzten Monat entdeckt hätte. Sie schlagen fehl, wenn sich das Verhalten in riskanter Weise ändert, und bleiben stabil bei harmlosen Refactorings. Ein High-Signal-Test kann zwanzig "gibt den erwarteten Wert zurück"-Checks ersetzen.
Niedrigwertige Happy-Path-Generierung hat meist einige deutliche Symptome:
- Viele Tests unterscheiden sich nur in Eingabelabels, nicht darin, was wirklich kaputtgehen kann.
- Assertions sind oberflächlich ("nicht null", "Status ist 200") statt den Sinn zu prüfen.
- Das Setup ist aufwändiger als das getestete Verhalten, sodass Leute aufhören, Tests zu aktualisieren.
- Coverage sieht hoch aus, aber Randfälle bleiben unberührt.
Stell dir eine Funktion vor, die einen Rabattcode anwendet. Happy-Path-Tests bestätigen, dass "SAVE10" den Preis reduziert. Reale Bugs verstecken sich anderswo: 0- oder negative Preise, abgelaufene Codes, Rundungsgrenzen oder maximale Rabattgrenzen. Das sind die Fälle, die zu falschen Gesamtsummen, verärgerten Kund:innen und nächtlichen Rollbacks führen.
Das Ziel ist, von „mehr Tests" zu „besseren Tests" zu wechseln, indem man drei Ziele anvisiert: Grenzen, Fehlerarten und Invarianten.
Die drei Ziele: Grenzen, Fehlerarten, Invarianten
Wenn du High-Signal-Unit-Tests willst, hör auf, nach „mehr Tests" zu fragen, und fordere drei konkrete Typen an. Das ist der Kern eines Claude Code Prompts, der nützliche Abdeckung liefert, anstatt einen Haufen „funktioniert bei normaler Eingabe"-Checks.
1) Grenzen (wo Bugs sich verstecken)
Grenzen sind die Ränder dessen, was der Code akzeptiert oder produziert. Viele reale Defekte sind Off-by-One-, Leerstates- oder Timeout-Probleme, die im Happy-Path nie auftreten.
Denk an Minimal- und Maximalwerte (0, 1, maximale Länge), leer vs. vorhanden ("", [], nil), Off-by-One (n-1, n, n+1) und Zeitlimits (nahe der Grenze).
Beispiel: Wenn eine API „bis zu 100 Items" akzeptiert, teste 100 und 101, nicht nur 3.
2) Fehlerarten (zeige, dass es sicher fehlschlägt)
Fehlerarten sind die Wege, wie das System kaputtgehen kann: ungültige Eingaben, fehlende Abhängigkeiten, partielle Ergebnisse oder Fehler flussaufwärts. Gute Tests für Fehlerarten prüfen das Verhalten unter Stress, nicht nur die Ausgabe unter idealen Bedingungen.
Beispiel: Wenn ein Datenbankaufruf fehlschlägt, gibt die Funktion dann einen klaren Fehler zurück und vermeidet partielle Schreibvorgänge?
3) Invarianten (Regeln, die immer gelten müssen)
Invarianten sind Wahrheiten, die vor und nach einem Aufruf bestehen sollten. Sie verwandeln vage Korrektheit in präzise Assertions.
Beispiele:
- „Kontostand wird nach einem Abhebungsversuch nie negativ“.
- „IDs sind einzigartig“, auch wenn Items schnell erstellt werden.
- „Bei Fehler kein Zustandswechsel“ (keine neuen Reihen, keine umgelegten Flags).
Wenn du dich auf diese drei Ziele konzentrierst, bekommst du weniger Tests, aber jeder trägt mehr Signal.
Vorbereitung: Extrahiere einen kleinen Vertrag bevor du Tests schreibst
Wenn du zu früh nach Tests fragst, erhältst du meist einen Haufen höflicher „funktioniert wie erwartet"-Checks. Eine einfache Lösung ist, zuerst einen winzigen Vertrag zu schreiben und dann Tests aus diesem Vertrag zu generieren. Das ist der schnellste Weg, um einen Claude Code Prompt in etwas zu verwandeln, das echte Bugs findet.
Ein nützlicher Vertrag ist kurz genug, um ihn auf einen Blick zu erfassen. Zielt auf 5 bis 10 Zeilen, die drei Fragen beantworten: Was kommt rein, was kommt raus, und was ändert sich sonst noch.
Eine 5–10 Zeilen Vertragsschablone
Schreibe den Vertrag in einfacher Sprache, nicht in Code, und nimm nur auf, was du testen kannst.
- Inputs: Typen, erlaubte Bereiche und was als „leer" oder „fehlend" zählt.
- Output: Rückgabewert oder Fehler-Shape und welche Garantien „Erfolg" bietet.
- Seiteneffekte: Änderungen am Zustand, Datenbankzeilen, Netzwerkanrufe, Dateien, Logs.
- Annahmen: Dinge, die Aufrufer:innen oft falsch machen (Zeitzone, Encoding, Auth, Reihenfolge).
- „Darf niemals passieren": Crash, stiller Datenverlust, Doppelbelastung, partielle Writes.
Sobald du das hast, scanne es danach, wo die Realität deine Annahmen brechen kann. Das werden Grenzfälle (Min/Max, Null, Überlauf, leere Strings, Duplikate) und Fehlerarten (Timeouts, Berechtigungsverweigerung, Unique-Constraint-Verstöße, beschädigte Eingaben).
Hier ein konkretes Beispiel für eine Funktion wie reserveInventory(itemId, qty):
Der Vertrag könnte sagen, qty muss eine positive Ganzzahl sein, die Funktion sollte atomar sein und niemals negativen Lagerbestand erzeugen. Das schlägt sofort High-Signal-Tests vor: qty = 0, qty = 1, qty größer als verfügbar, parallele Aufrufe und ein absichtlich ausgelöster Datenbankfehler in der Mitte.
Wenn du ein Chat-basiertes Tool wie Koder.ai verwendest, gilt derselbe Workflow: Schreibe zuerst den Vertrag im Chat, dann generiere Tests, die direkt Grenzen, Fehlerarten und die „darf niemals passieren"-Liste angreifen.
Prompt-Muster: Die High-Signal Test-Blueprint
Verwende diesen Claude Code Test-Generierungs-Prompt, wenn du weniger Tests willst, aber jeder Test Gewicht hat. Die Schlüsselaktion ist, zuerst einen Testplan zu erzwingen und dann Testcode erst nach Freigabe des Plans zu generieren.
You are helping me write HIGH-SIGNAL unit tests.
Context
- Language/framework: <fill in>
- Function/module under test: <name + short description>
- Inputs: <types, ranges, constraints>
- Outputs: <types + meaning>
- Side effects/external calls: <db, network, clock, randomness>
Contract (keep it small)
1) Preconditions: <what must be true>
2) Postconditions: <what must be true after>
3) Error behavior: <how failures are surfaced>
Task
PHASE 1 (plan only, no code):
A) Propose 6-10 tests max. Do not include “happy path” unless it protects an invariant.
B) For each test, state: intent, setup, input, expected result, and WHY it is high-signal.
C) Invariants: list 3-5 invariants and how each will be asserted.
D) Boundary matrix: propose a small matrix of boundary values (min/max/empty/null/off-by-one/too-long/invalid enum).
E) Failure modes: list negative tests that prove safe behavior (no crash, no partial write, clear error).
Stop after PHASE 1 and ask for approval.
PHASE 2 (after approval):
Generate the actual test code with clear names and minimal mocks.
Ein praktischer Trick ist, die Boundary-Matrix als kompakte Tabelle zu verlangen, damit Lücken offensichtlich werden:
| Dimension | Valid edge | Just outside | “Weird” value | Expected behavior |
|---|---|---|---|---|
| length | 0 | -1 | 10,000 | error vs clamp vs accept |
Wenn Claude 20 Tests vorschlägt, wehre dich. Fordere es auf, ähnliche Fälle zusammenzuführen und nur die zu behalten, die einen echten Bug fangen würden (Off-by-One, falscher Fehlertyp, stiller Datenverlust, gebrochene Invariante).
Schritt-für-Schritt: Führe den Prompt aus und verwandle Ausgabe in Tests
Beginne mit einem kleinen, konkreten Vertrag für das Verhalten, das du willst. Füge die Funktionssignatur, eine kurze Beschreibung der Ein- und Ausgaben und vorhandene Tests (auch wenn sie nur Happy-Path sind) ein. Das hält das Modell an dem, was der Code tatsächlich macht, statt an dem, was es vermutet.
Als Nächstes fordere eine Risikotabelle, bevor du Testcode verlangst. Bestehe auf drei Spalten: Grenzfälle (Eingabegrenzen), Fehlerarten (schlechte Eingaben, fehlende Daten, Timeouts) und Invarianten (Regeln, die immer gelten sollten). Füge pro Zeile einen Satz hinzu: „warum das brechen kann." Eine einfache Tabelle legt Lücken schneller offen als ein Haufen Testdateien.
Dann wähle die kleinste Menge von Tests, bei der jeder Test einen einzigartigen Bug-Fang-Zweck hat. Wenn zwei Tests aus demselben Grund fehlschlagen würden, behalte den stärkeren.
Eine praktische Auswahlregel:
- Behalte Tests, die unterschiedliche Grenzen treffen (Min, Max, leer, Off-by-One).
- Behalte Tests, die sicheres Verhalten bei Fehlern beweisen (klarer Fehler, kein partielles Write, kein Crash).
- Behalte Tests, die eine Invariante prüfen (Reihenfolge, Summen, Idempotenz, keine Duplikate).
- Schneide Tests weg, die nur wiederholen „funktioniert mit normaler Eingabe".
Schließlich verlange für jeden Test eine kurze Erklärung: Welchen Bug würde er fangen, wenn er fehlschlägt. Wenn die Erklärung vage ist ("validates behavior"), ist der Test wahrscheinlich Low-Signal.
Wie man Invarianten in Assertions kodiert
Eine Invariante ist eine Regel, die unabhängig von gültigen Eingaben wahr bleiben sollte. Beim invariant-basierten Testen schreibst du zuerst die Regel in einfacher Sprache und wandelst sie dann in eine Assertion um, die laut fehlschlägt.
Wähle 1 oder 2 Invarianten, die dich wirklich vor realen Bugs schützen. Gute Invarianten betreffen oft Sicherheit (kein Datenverlust), Konsistenz (gleiche Eingaben → gleiche Ausgabe) oder Grenzen (Kappen nie überschreiten).
Verwandle eine Invariante in einen prüfbaren Check
Formuliere die Invariante kurz, und entscheide dann, welche Belege dein Test beobachten kann: Rückgabewerte, gespeicherte Daten, ausgesendete Events oder Aufrufe von Abhängigkeiten. Starke Assertions prüfen sowohl Ergebnis als auch Seiteneffekte, weil viele Bugs in „gibt OK zurück, hat aber das Falsche geschrieben" versteckt sind.
Beispiel: Angenommen, eine Funktion wendet einen Coupon auf eine Bestellung an:
- Invariante: der Endbetrag ist nie negativ.
- Invariante: derselbe Coupon wird nicht doppelt angewendet.
Jetzt kodierst du das als konkrete Assertions:
expect(result.total).toBeGreaterThanOrEqual(0)
expect(db.getOrder(orderId).discountCents).toBe(originalDiscountCents)
Vermeide vage Assertions wie "returns expected result". Prüfe die spezifische Regel (nicht negativ) und den spezifischen Seiteneffekt (Rabatt nur einmal gespeichert).
Füge eine Gegenbeispiel-Notiz hinzu, damit der Test scharf bleibt
Für jede Invariante füge eine kurze Notiz im Test hinzu, welches Datum sie verletzen würde. Das verhindert, dass der Test später zu einem Happy-Path-Check verkommt.
Ein einfaches Muster, das sich bewährt:
- Setze die Invariante in den Testnamen.
- Prüfe die Invariante im Ergebnis.
- Prüfe den wesentlichen Seiteneffekt (oder dessen Fehlen).
- Füge einen Kommentar mit einem Verletzungsbeispiel (z. B. sehr großer Coupon-Wert oder doppelte Anwendung).
Fehlerarten: Schreibe Tests, die sicheres Fehlverhalten belegen
High-Signal-Tests sind häufig diejenigen, die bestätigen, dass dein Code sicher fehlschlägt. Wenn ein Modell nur Happy-Path-Tests schreibt, erfährst du wenig darüber, wie das Feature sich verhält, wenn Eingaben und Abhängigkeiten unordentlich werden.
Beginne damit, zu definieren, was „sicher" für dieses Feature bedeutet. Gibt es einen typisierten Fehler? Fällt es auf einen Default zurück? Versucht es genau einmal neu und stoppt dann? Schreibe dieses erwartete Verhalten in einem Satz und lass die Tests es beweisen.
Wenn du Claude Code um Tests für Fehlerarten bittest, halte das Ziel strikt: Decke die Wege ab, wie das System brechen kann, und behaupte die genaue gewünschte Antwort. Eine nützliche Zeile ist: „Weniger Tests mit stärkeren Assertions sind besser als viele flache Tests."
Fehlerkategorien, die tendenziell die besten Tests liefern:
- Schlechte Eingaben: ungültige Formate, fehlende Pflichtfelder, Werte außerhalb des Bereichs
- Abhängigkeitsfehler: Timeouts, 500s, leere Antworten, beschädigte Payloads
- Reihenfolgeprobleme: out-of-order Events, Duplikate, partielle Writes
- Konkurrenz: Racing-Updates, Idempotenzprüfungen
- Wiederherstellungsverhalten: wann gibst du einen Fehler zurück vs. fällst du zurück vs. versuchst du neu
Beispiel: Du hast einen Endpoint, der einen Nutzer erstellt und einen E-Mail-Service aufruft, um eine Willkommensnachricht zu senden. Ein Low-Value-Test prüft nur "returns 201". Ein High-Signal-Fehlertest prüft, dass wenn der E-Mail-Service time-outet, du entweder (a) den Nutzer trotzdem anlegst und 201 mit einem "email_pending"-Flag zurückgibst, oder (b) ein klares 503 zurückgibst und den Nutzer nicht anlegst. Wähle ein Verhalten und prüfe sowohl die Antwort als auch die Seiteneffekte.
Teste auch, was du nicht leakst. Wenn die Validierung fehlschlägt, stelle sicher, dass nichts in die Datenbank geschrieben wurde. Wenn eine Abhängigkeit eine beschädigte Payload zurückgibt, sorge dafür, dass du keine unbehandelten Exceptions oder rohe Stacktraces zurückgibst.
Häufige Fallen, die niedrigwertige Tests erzeugen
Niedrigwertige Test-Sets entstehen oft, wenn das Modell für Masse belohnt wird. Wenn dein Claude Code Prompt nach "20 Unit Tests" fragt, bekommst du oft kleine Variationen, die gründlich aussehen, aber nichts Neues fangen.
Häufige Fallen:
- Look-alike-Tests: derselbe "gültige Input"-Test mit unterschiedlichen Strings oder Zahlen wiederholt.
- Tests, die den Code spiegeln: Assertions auf private Schritte oder Hilfsaufrufe statt beobachtbares Verhalten.
- Alles mocken: DB, Clock, Netzwerk und Config gleichzeitig ersetzen.
- Schwache Assertions: nur prüfen "kein Fehler", "nicht null" oder "Status 200".
- Schmutziger geteilter Zustand: hinterlassene Seed-Daten, veränderte Globals oder gecachte Werte.
Beispiel: Stell dir eine create user-Funktion vor. Zehn Happy-Path-Tests könnten das E-Mail-String variieren und trotzdem das Wichtige verpassen: Duplikat-E-Mails abweisen, leeres Passwort behandeln und sicherstellen, dass zurückgegebene Nutzer-IDs eindeutig und stabil sind.
Schutzmaßnahmen, die bei Reviews helfen:
- Verlange, dass jeder Test das Risiko benennt, das er abdeckt (Grenze, Fehlerart oder Invariante).
- Vermeide nur-Implementierungsprüfungen, es sei denn, sie ändern beobachtbares Verhalten.
- Halte Mocks minimal und erlaube eine kleine Anzahl von Tests, die die echte Integration treffen, wenn das praktikabel ist.
- Fordere starke Assertions: exakte Ausgaben, Zustandsänderungen und Fehlertypen/-nachrichten.
- Füge Cleanup-Regeln hinzu, damit Tests nicht in einer bestimmten Reihenfolge abhängen.
Beispiel: Eine Funktion in eine kleine, starke Testmenge verwandeln
Stell dir ein Feature vor: Einen Coupon-Code im Checkout anwenden.
Contract (klein und testbar): Gegeben ein Warenkorb-Subtotal in Cent und ein optionaler Coupon, gib einen finalen Total-Betrag in Cent zurück. Regeln: Prozent-Coupons runden nach unten auf den nächsten Cent, fixe Coupons ziehen einen festen Betrag ab und Totals werden niemals negativ. Ein Coupon kann ungültig, abgelaufen oder bereits verwendet sein.
Frage nicht einfach: "Tests für applyCoupon()". Fordere Grenzfalltests, Fehlerarten und Invarianten, die an diesen Vertrag gebunden sind.
Grenzen, die Randverhalten erzwingen
Wähle Eingaben, die Mathe oder Validierung typischerweise brechen: ein leerer Coupon-String, subtotal = 0, subtotal knapp unter und über einem Mindestbetrag, ein fixer Rabatt größer als das Subtotal und ein Prozentwert wie 33%, der Rundung erzeugt.
Fehlerarten, die sicheres Verhalten beweisen
Angenommen, die Coupon-Suche kann fehlschlagen und Zustand kann falsch sein: Der Coupon-Service ist down, der Coupon ist abgelaufen oder wurde bereits von diesem Nutzer eingelöst. Der Test sollte zeigen, was dann passiert (Coupon abgelehnt mit klarem Fehler, Total unverändert).
Eine minimale, High-Signal-Testmenge (5 Tests) und was jeder aufdeckt:
- Leere oder nur aus Whitespace bestehende Codes ablehnen: fängt Fehler, die leere Codes als gültig akzeptieren und schlechte Trimm-Logik aufdecken.
- Prozent-Coupon-Rundung (subtotal 101, 33%): fängt Rundungsfehler und Off-by-One-Cents.
- Fixer Rabatt größer als Subtotal (subtotal 500, discount 1000): beweist die Invariante, dass das Total niemals negativ wird.
- Mindestbestellwert-Grenze (subtotal 999 vs 1000): fängt falsche Vergleichslogik (< vs <=).
- Coupon-Lookup-Fehler oder Timeout: beweist sicheres Fallback (kein Rabatt angewendet) und stabiles Fehlerverhalten.
Wenn diese Tests bestehen, hast du die üblichen Bruchstellen abgedeckt, ohne die Suite mit doppelten Happy-Path-Tests zu füllen.
Kurzcheckliste für High-Signal KI-generierte Tests
Bevor du akzeptierst, was das Modell generiert hat, mach eine kurze Qualitätsprüfung. Das Ziel sind Tests, die dich jeweils vor einem spezifischen, wahrscheinlichen Bug schützen.
Benutze diese Checkliste als Tor:
- Grenzen pro Eingabefeld: Für jedes Eingabefeld (Strings, IDs, Timestamps, Flags) mindestens einen Randfall (leer vs nur Whitespace, Max-Länge, 0 vs negativ, fehlende optionale Felder, eins über dem Limit).
- Abhängigkeitsfehler: Mindestens ein Test, in dem eine Abhängigkeit sich falsch verhält (DB-Timeout, Drittanbieter-API 500, abgelaufenes Auth-Token). Beweise sicheres Verhalten (klarer Fehler, keine partiellen Writes).
- Invarianten mit starken Assertions: Wähle 1–3 Regeln, die immer gelten müssen, und prüfe sie direkt. Vermeide vage Assertions wie "response is ok".
- Ein einzigartiger Bug pro Test: Lies jeden Testtitel und frage: "Welchen genauen Bug würde dieser fangen?" Wenn zwei Tests dieselbe Frage beantworten, merge sie.
- Entfernen-Test: Versuch, einen Test zu löschen. Wenn nichts Bedeutendes verloren geht (keine Grenze, keine Fehlerart, keine Invariante), hatte er keinen Wert.
Ein praktischer Trick nach der Generierung: Benenne Tests zu „should <Verhalten> when <Randbedingung>" und „should not <schlechtes Ergebnis> when <Fehler>". Wenn du sie nicht sauber umbenennen kannst, sind sie nicht fokussiert.
Wenn du mit Koder.ai arbeitest, passt diese Checkliste gut zu Snapshots und Rollback: generiere Tests, führ sie aus und rolle zurück, wenn die neue Menge Lärm hinzufügt ohne Coverage zu verbessern.
Nächste Schritte: Mach das zu einem wiederholbaren Workflow
Behandle deinen Prompt als wiederverwendbares Werkzeug, nicht als One-Off. Speichere eine Blueprint-Vorlage (die das Erzwingen von Grenzen, Fehlerarten und Invarianten beinhaltet) und benutze sie für jede neue Funktion, jeden Endpoint oder Flow.
Eine einfache Gewohnheit, die Ergebnisse schnell verbessert: Fordere einen Satz pro Test, der erklärt, welchen Bug er fangen würde. Wenn der Satz generisch ist, ist der Test vermutlich Rauschen.
Führe eine lebende Liste von Domain-Invarianten für dein Produkt. Speichere sie nicht nur im Kopf. Ergänze sie, wann immer du einen echten Bug findest.
Ein leichter Workflow, den du wiederholen kannst:
- Extrahiere einen winzigen Vertrag: Inputs, Outputs, Fehlermanagement und 3–5 Invarianten.
- Führe das Blueprint-Prompt aus und fordere Grenzen, Fehlerarten, Invarianten sowie einzeilige Begründungen an.
- Implementiere nur die Top 5–10 Tests, die unterschiedliche Risiken abdecken.
- Refaktoriere und führe das Prompt erneut aus, um zu sehen, welche neuen Risiken auftauchen.
- Kürze Duplikate und behalte Tests, die vergangene Vorfälle gefunden hätten.
Wenn du Apps per Chat baust, führe diesen Zyklus in Koder.ai (koder.ai) aus, damit Vertrag, Plan und generierte Tests an einem Ort bleiben. Wenn ein Refactor das Verhalten unerwartet ändert, machen Snapshots und Rollback es leichter, zu vergleichen und iterativ so lange zu arbeiten, bis deine High-Signal-Menge stabil bleibt.
FAQ
How many unit tests should I generate per function?
Default: aim for a small set that would catch a real bug.
A quick cap that works well is 6–10 tests per unit (function/module). If you need more, it usually means your unit is doing too much or your contract is unclear.
What’s wrong with generating lots of happy-path tests?
Happy-path tests mostly prove that your example still works. They tend to miss the stuff that breaks in production.
High-signal tests target:
- Boundaries (0/1/max, empty/null, off-by-one)
- Failure modes (timeouts, invalid inputs, dependency errors)
- Invariants (rules that must always hold, like “no partial write on error”)
What should I write down before asking an AI to generate tests?
Start with a tiny contract you can read in one breath:
- Inputs: types, allowed ranges, what counts as empty/missing
- Outputs: success shape and error shape
- Side effects: what can be written/changed (DB, files, network)
- “Must never happen”: crash, silent data loss, double charge, partial writes
Then generate tests from that contract, not from examples alone.
Which boundary cases are usually worth testing?
Test these first:
- Min/max values (0, 1, max, max+1)
- Empty vs present ("", [], null/nil)
- Off-by-one (n-1, n, n+1)
- Formatting edges (whitespace-only strings, leading zeros)
- Time cutoffs (just before/after expiry)
Pick one or two per input dimension so each test covers a unique risk.
How do I write a good “failure mode” test instead of a shallow one?
A good failure-mode test proves two things:
- The function returns a clear, expected error (type/message/status).
- It fails safely:
- no partial state changes
- no leaked internal details
- no retries or side effects you didn’t intend
If there’s a database write involved, always check what happened in storage after the failure.
How do I turn an invariant into a test assertion?
Default approach: turn the invariant into an assertion on observable outcomes.
Examples:
- “Total never negative” →
expect(total).toBeGreaterThanOrEqual(0) - “On error, no state changes” → assert no new rows / no flags flipped
- “Idempotent” → call twice and assert the second call doesn’t change state
Prefer checking both return value and side effects, because many bugs hide in “returned OK but wrote the wrong thing.”
When is a happy-path test still worth writing?
It’s worth keeping a happy-path test when it protects an invariant or a critical integration.
Good reasons to keep one:
- It asserts a key invariant on normal input (e.g., rounding rules)
- It locks down an API contract that callers rely on
- It guards against a past incident regression
Otherwise, trade it for boundary/failure tests that catch more classes of bugs.
What should I ask the model to output before generating test code?
Push for PHASE 1: plan only first.
Require the model to provide:
- 6–10 proposed tests max
- For each: intent, setup, input, expected result, why it’s high-signal
- A small boundary matrix
- A failure-mode list
- 3–5 invariants and how to assert them
Only after you approve the plan should it generate code. This prevents “20 look-alike tests” output.
How do I avoid tests that are brittle because they mock too much?
Default: mock only the boundary you don’t own (DB/network/clock), and keep everything else real.
To avoid over-mocking:
- Don’t mock internal helpers just to mirror implementation
- Use a real in-memory version when feasible, or a small fake with clear behavior
- Mock the clock/randomness only when it affects the assertion
If a test breaks on refactor but behavior didn’t change, it’s often over-mocked or too implementation-coupled.
How can I quickly tell if an AI-generated test is low-value?
Use a simple deletion test:
- If you delete the test and lose no boundary, no failure mode, and no invariant, it didn’t earn its place.
Also scan for duplicates:
- If two tests would fail for the same bug, keep the one with the stronger assertion.
- If assertions are just “not null” or “status 200,” strengthen them or remove the test.