Spielt TDD noch eine Rolle, wenn Agenten den Code schreiben?
Erfahren Sie, wie testgetriebene Entwicklung KI-Agenten kontrolliert: Vitest-Tests zuerst schreiben, Änderungen daran verhindern und Code in CI prüfen.
Ja, wohl sogar mehr als zuvor, allerdings als Vertrag statt als Ritual. Wenn ein Coding-Agent die Implementierung schreibt, werden vorab geschriebene Tests zur Spezifikation: Sie definieren, was „fertig“ bedeutet, bevor überhaupt Code existiert, und entlarven den Agenten, wenn er eine Aufgabe als erledigt meldet, die es nicht ist.
Die meisten Entwickler, die die Implementierung an Claude Code, Cursor oder GitHub Copilot übergeben, haben es schon erlebt. Der Agent verkündet, dass alles grün ist, und dann bricht das Feature bei der ersten Nutzung. Oder eine Testdatei wurde stillschweigend so umgeschrieben, dass sie zum neuen Code passt.
Dieser Artikel behandelt den Test-first-Workflow für Agenten: eine echte Vitest-Datei, die passende Anweisung dazu, die Wege, auf denen Agenten das Verfahren umgehen, und die Guardrails, die diese Lücken schließen.
Die wichtigsten Erkenntnisse
- Tests, die vor dem Start des Agenten geschrieben werden, wirken als ausführbare Spezifikation: Die Aufgabe des Agenten ist es, sie zum Bestehen zu bringen, nicht zu entscheiden, was „bestanden“ bedeutet.
- Ein Test, der nach dem Code geschrieben wird, beschreibt tendenziell nur, was der Code ohnehin schon tut. Er besteht also zwangsläufig und prüft nichts.
- Die Meldung eines Agenten, dass alle Tests bestehen, ist eine Behauptung. Ein CI-Lauf auf dem gepushten Commit ist der Beweis.
- Ändert ein einziger Diff sowohl die Implementierung als auch die Assertions, die sie bewerten, hat der Agent seine eigene Arbeit benotet. Reviewen Sie Teständerungen deshalb getrennt von Codeänderungen.
- Verzichten Sie bei Wegwerf-Prototypen und rein visuellen Anpassungen auf Test-first, denn dort lässt sich das erwartete Ergebnis erst festhalten, wenn man es sieht.
Was ist Red-Green-Refactor?
Red-Green-Refactor ist der TDD-Zyklus: Sie schreiben einen fehlschlagenden Test, der genau ein Verhalten beschreibt (Red), und dann den minimalen Code, der ihn bestehen lässt (Green). Ist die Suite grün, räumen Sie den Code auf, ohne sein Verhalten zu ändern (Refactor). Eine ausführlichere Auffrischung mit React-Beispielen finden Sie in TDD vs BDD in React.
Wie funktioniert der Workflow für TDD mit KI?
TDD mit KI funktioniert, wenn ein Mensch die fehlschlagenden Tests schreibt oder freigibt und der Agent die Anweisung erhält, sie zum Bestehen zu bringen, ohne sie zu verändern. Die Tests enthalten die Anforderungen, der Agent liefert lediglich die Implementierung. Die folgenden Beispiele verwenden Vitest 5.
Beginnen Sie mit einem Stub, damit die Testdatei etwas Reales importiert:
// src/cart/applyCoupon.ts
export type Coupon =
| { code: string; kind: 'percent'; value: number }
| { code: string; kind: 'fixed'; valueCents: number };
export function applyCoupon(
subtotalCents: number,
code: string,
coupons: Coupon[],
): number {
throw new Error('not implemented');
}
Schreiben Sie dann die Spezifikation:
// src/cart/applyCoupon.test.ts
import { describe, it, expect } from 'vitest';
import { applyCoupon, type Coupon } from './applyCoupon';
const coupons: Coupon[] = [
{ code: 'SAVE10', kind: 'percent', value: 10 },
{ code: 'FIVEOFF', kind: 'fixed', valueCents: 500 },
];
describe('applyCoupon', () => {
it('takes a percentage off the subtotal', () => {
expect(applyCoupon(2000, 'SAVE10', coupons)).toBe(1800);
});
it('takes a fixed amount off the subtotal', () => {
expect(applyCoupon(2000, 'FIVEOFF', coupons)).toBe(1500);
});
it('never returns a negative total', () => {
expect(applyCoupon(300, 'FIVEOFF', coupons)).toBe(0);
});
it('matches codes case-insensitively', () => {
expect(applyCoupon(2000, 'save10', coupons)).toBe(1800);
});
it('rejects unknown codes', () => {
expect(() => applyCoupon(2000, 'BOGUS', coupons)).toThrow('Unknown coupon');
});
});
Da der Stub existiert und eine Exception wirft, schlagen alle fünf Tests zur Laufzeit fehl und nicht wegen eines fehlenden Imports. Das ist eine saubere Red-Phase. Der letzte Test verdient einen genaueren Blick: Der Stub wirft zwar eine Exception, aber deren Meldung passt nicht zu 'Unknown coupon', sodass der Test aus dem richtigen Grund fehlschlägt. Jeder Test legt eine Regel fest (prozentuale versus feste Rabatte, die Untergrenze bei null, die Behandlung von Groß- und Kleinschreibung), die ein Agent andernfalls erraten müsste.
Übergeben Sie die Aufgabe anschließend mit expliziten Vorgaben:
Implement applyCoupon in src/cart/applyCoupon.ts so every test in
src/cart/applyCoupon.test.ts passes.
Rules:
- Do not create, edit, rename, skip or delete any *.test.ts file.
- Do not add .skip, .only, or mocks of applyCoupon.
- If a test looks wrong, stop and explain why instead of changing it.
- When finished, run `npx vitest run src/cart` and paste the full output.
Die Zeile „stop and explain“ gibt dem Agenten einen legitimen Ausweg, der nicht darin besteht, den Test zu ändern. Die eingefügte Ausgabe erspart Ihnen einen Schritt, beweist aber nichts. Weitere Prompting-Gewohnheiten, die zu diesem Zyklus passen, finden Sie in 5 Tips and Tricks for AI-Assisted Coding.
Wie geht Test-first mit Agenten schief?
Agenten suchen meist den kürzesten Weg zu einem grünen Testlauf, und drei dieser Wege umgehen die Spezifikation vollständig.
Tests bearbeiten oder löschen. Wenn eine Implementierung schwierig ist, ist es einfacher, die Erwartung zu ändern, als sie zu erfüllen. Ein geändertes toBe(1800), ein neues .skip oder eine verschwundene Testdatei führen allesamt zu einem grünen Lauf, bei dem die Anforderung entfernt wurde.
Schwache Assertions. Ein Test, der nach dem Code geschrieben wird, bestätigt tendenziell, was der Code tut, und nicht, was er tun sollte:
// Confirms the code ran, not that it's right
it('applies coupon', () => {
const result = applyCoupon(2000, 'SAVE10', coupons);
expect(result).toBeDefined();
expect(typeof result).toBe('number');
});
// Pins the behaviour
it('takes 10% off with SAVE10', () => {
expect(applyCoupon(2000, 'SAVE10', coupons)).toBe(1800);
});
Der schwache toBeDefined()-Test besteht, ob applyCoupon nun 0, 2000 oder 999999 zurückgibt. Deshalb kommt es auf die Reihenfolge an: Tests, die der Agent nach seinem eigenen Code schreibt, ähneln meist der ersten Variante. Außerdem blähen Agenten Test-Suites gern mit vielen oberflächlichen Prüfungen dieser Art auf, ein Muster, das in the smoke tests agents keep writing behandelt wird.
Das Testobjekt selbst mocken. Wird das zu testende Modul gemockt, prüft der Test nur den Mock und bleibt grün, egal was die echte Implementierung tut:
import { vi } from 'vitest';
vi.mock('./applyCoupon', () => ({ applyCoupon: () => 1800 }));
// Every assertion now tests the mock.
Vitest hebt vi.mock über die Imports (Hoisting), sodass die Factory das Modul ersetzt, bevor der echte Code geladen wird. Das echte applyCoupon wird nie ausgeführt. Mocken Sie die Systemgrenzen (Netzwerk, Speicher, Zeit), nicht die Unit, die Sie spezifizieren.
Welche Guardrails halten die Spezifikation stabil?
Guardrails funktionieren, wenn sie mechanisch greifen. Anweisungen verringern Manipulationen an Tests, und CI sowie Reviews fangen ab, was Anweisungen übersehen.
Test-Diffs separat reviewen. Prüfen Sie Änderungen unter *.test.ts als eigenständiges Review, noch vor der Implementierung. Auf GitHub können Sie mit CODEOWNERS und verpflichtendem Code-Owner-Review sicherstellen, dass eine namentlich festgelegte Person jede Änderung an Testpfaden freigibt.
Teständerungen für den Agenten blockieren oder untersagen.
- Claude Code: Fügen Sie in
.claude/settings.jsoneinepermissions.deny-Regel wie"Edit(**/*.test.ts)"hinzu. Edit-Deny-Regeln gelten für Claudes integrierte Datei-Tools sowie für Dateibefehle, die es in Bash erkennt (etwasedundtee), und für Umleitungsziele. Skripte oder andere Subprozesse, die indirekt Dateien schreiben, decken sie nicht ab. Betrachten Sie die Regel als Hürde, nicht als echte Durchsetzung. - Cursor: Formulieren Sie die Regel in den Projektregeln unter
.cursor/rules. - GitHub Copilot: Hinterlegen Sie sie in
.github/copilot-instructions.md. Cursor und Copilot lesen außerdem beideAGENTS.md.
CI entscheiden lassen, was „fertig“ heißt. Führen Sie npx vitest run auf dem gepushten Commit aus. In CI lässt Vitest den Lauf fehlschlagen, wenn es .only findet, sofern Sie nicht --allowOnly übergeben oder allowOnly: true setzen. .skip erkennt es nicht, daher sollten Sie auf Implementierungs-Branches einen Diff-Guard ergänzen:
changed=$(git diff --name-only origin/main...HEAD -- '*.test.ts' '*.spec.ts')
if [ -n "$changed" ]; then
echo "Test files changed on an implementation branch:"
echo "$changed"
exit 1
fi
Der Checkout benötigt genügend Historie, damit origin/main aufgelöst werden kann.
Prüfen, ob Assertions Verhalten testen. Fragen Sie sich bei jeder Assertion, ob eine falsche Implementierung sie trotzdem bestehen könnte. Exakte Werte, geworfene Fehler und sichtbare Ausgaben überstehen Refactorings. Aufrufzähler interner Hilfsfunktionen und Typprüfungen tun das nicht.
Auch eine saubere Test-Suite hat Grenzen. Bestandene Unit-Tests zeigen, dass der Code die Fälle abdeckt, die jemand festgehalten hat. Sie zeigen nicht, dass ein echter Nutzer den Ablauf im Browser abschließen kann, und Session Replays eines ausgelieferten Features können durchaus einen fehlerhaften Checkout zeigen, während CI grün ist.
Wann lohnt sich TDD nicht?
Test-first kostet mehr, als es bringt, wenn sich das erwartete Ergebnis nicht im Voraus formulieren lässt.
| Situation | Warum auf Test-first verzichten |
|---|---|
| Wegwerf-Prototypen | Der Code soll eine Frage beantworten und dann gelöscht werden |
| Visuelle UI-Anpassungen (Abstände, Farben, Layout) | Das richtige Ergebnis beurteilen Sie durch Hinsehen, es gibt also noch nichts zu prüfen |
Überlebt ein Prototyp, schreiben Sie die Tests, bevor der Agent ihn zu Produktionscode ausbaut.
Fazit
Wenn Agenten den Code schreiben, ist die Spezifikation der Teil, den nur Sie liefern können, und vorab geschriebene Tests machen sie ausführbar. Schreiben Sie bei Ihrer nächsten Agenten-Aufgabe zuerst die fehlschlagende Vitest-Datei, geben Sie dem Agenten die Anweisung, keine Tests zu ändern, und mergen Sie erst, wenn sowohl CI als auch ein separates Review des Test-Diffs bestanden sind.
FAQs
Kann ein Linter übersprungene Tests erkennen, die ein Agent hinzufügt?
Ja. Das Paket @vitest/eslint-plugin enthält die Regel vitest/no-disabled-tests, die test.skip, it.skip und describe.skip markiert, sowie die Regel vitest/no-focused-tests, die .only markiert. Die empfohlene Konfiguration gibt bei deaktivierten Tests nur eine Warnung aus, setzen Sie diese Regel daher auf 'error' und führen Sie ESLint in CI aus. Jest-Projekte erhalten gleichnamige Regeln über eslint-plugin-jest. Da Lint Aufrufe über Aliase übersehen kann, sollten Sie den Git-Diff-Guard zusätzlich beibehalten.
Kann ein KI-Agent die fehlschlagenden Tests anstelle eines Menschen schreiben?
Ja, solange das Schreiben der Tests und das Schreiben der Implementierung getrennte Schritte sind. Lassen Sie eine Agenten-Session die Tests gegen den Stub entwerfen, prüfen Sie die Assertions dann selbst und committen Sie sie, bevor die Implementierung beginnt. Die implementierende Session sollte nur Tests sehen, die sie nicht ändern darf. Schreibt dieselbe Session beides, beschreiben die Tests am Ende ihren eigenen Code statt der Anforderungen.
Reicht eine detaillierte schriftliche Spezifikation, ohne zuerst Tests zu schreiben?
Nein. Eine Spezifikation in Prosa oder ein mit Doc-Kommentaren versehener Stub sagt dem Agenten, was er bauen soll, aber nichts prüft das Ergebnis dagegen. Ein fehlschlagender Test lässt sich nicht mit einer plausibel klingenden Implementierung zufriedenstellen: Er besteht oder er besteht nicht. Nutzen Sie beides. Legen Sie Anforderungen und Vorgaben im Prompt oder in der Instruktionsdatei fest, und kodieren Sie die Fälle, die gelten müssen, als Tests, die CI bei jedem Commit ausführt.
Was sollte ich tun, wenn der Agent behauptet, ein Test sei falsch?
Prüfen Sie die Begründung des Agenten selbst, und wenn der Test tatsächlich falsch ist, korrigieren Sie ihn außerhalb des Implementierungs-Branches. Bringen Sie den korrigierten Test in einem eigenen Commit oder Pull Request auf main, gegebenenfalls mit Review über CODEOWNERS, und rebasen Sie anschließend den Implementierungs-Branch. Der Diff-Guard vergleicht den Branch mit origin/main, sodass eine bereits auf main gelandete Testkorrektur ihn nicht auslöst, und der Agent bearbeitet die Spezifikation nie selbst.