Beliebte Suchanfragen
//

16000 Tests in 4 Tagen - Mit Claude Code zu 80% Testabdeckung

5.5.2026 | 10 Minuten Lesezeit

Die Ausgangssituation

Als wir bei codecentric vor Kurzem bei einem Kunden die Codebasis eines früheren Dienstleisters übernahmen, wurde uns schnell klar, dass dies keine alltägliche Herausforderung werden würde. Backends, Frontends, Batches, Services, eine gewachsene Anwendungslandschaft eines Kunden, die über Jahre Schicht für Schicht aufgebaut worden war. Das zentrale Backend allein umfasste mehr als 70 Projekte und knapp 350.000 Zeilen produktiven Codes. Dazu kamen zahlreiche weitere Services, die das Gesamtbild komplettierten. Was uns erwartete, kennen viele Entwicklerteams aus eigener Erfahrung: eine Codebasis, die über Jahre gewachsen ist, und eine Testlandschaft, die mit diesem Wachstum nicht immer mithalten konnte.

Die Situation der vorhandenen Tests war alles andere als befriedigend. Zwar existierten Tests, aber ein erheblicher Teil davon lief nicht mehr fehlerfrei durch. Noch problematischer war, dass die Tests weder automatisiert noch regelmäßig in der CI/CD-Pipeline ausgeführt wurden. Das hatte zur Folge, dass der Verfall der Tests unbemerkt blieb. Unser erster Schritt war es, diesen Zustand zu bereinigen, alle fehlgeschlagenen Tests zu reparieren und eine stabile Ausgangsbasis zu schaffen. Das Ergebnis dieser Arbeit: eine Line Coverage von rund 58% mit 6646 Tests. Unser Ziel war 80%. Dabei ging es nicht um die Validierung der Korrektheit des bestehenden Produktionscodes, sondern zunächst darum, den Status Quo mit Tests abzusichern und unerwartete Änderungen bei späteren Anpassungen des Produktionscodes besser zu erkennen.

Bei mehreren hunderttausend Zeilen Produktionscode bedeutet das, mindestens die gleiche Menge Testcode zu schreiben. Da wir Claude Code ohnehin als KI-gestützten Assistenten im Projektalltag einsetzen, lag es nahe, die Testgenerierung damit anzugehen.

Wir haben diesen Ansatz in einem .Net-Projekt angewendet. Abgesehen von einigen kleineren .Net-spezifischen Herausforderungen ist unser Vorgehen grundsätzlich technologieunabhängig.

Der erste Versuch: zu groß gedacht

Unser erster Prompt war denkbar einfach gehalten: Claude soll Unit Tests für alle Projekte schreiben, bestehende Testklassen erweitern, neue anlegen, wo noch keine vorhanden sind und dabei 90% Line Coverage anstreben. 90% haben wir bewusst vorgegeben, um einen Puffer zu haben und schlechte Tests löschen zu können. Was dann passierte, war lehrreich, wenn auch auf die unangenehme Art.

Prompt:

Schreibe Unit Tests für alle Projekte in der Projektmappe. Ziel ist es 90% Line Coverage zu erreichen. Schaue dir die bestehenden Projekte und Projektstruktur an und schreibe die Tests analog zu den bestehenden Unit Tests. Wenn für die zu testenden Klassen bereits Tests existieren, erweitere die entsprechende Testklasse, andernfalls lege analog zu den anderen Testdateien neue Testdateien an. Der Produktionscode darf dabei nicht verändert werden.

Claude begann damit, eigenen Code zu schreiben, um die Line Coverage zu messen, obwohl das Projekt dafür bereits eine Methode enthielt. Wir ignorierten das zunächst und nahmen an, das Ergebnis würde sich nicht wesentlich unterscheiden. Das war ein Fehler! Claude generierte Tests in Iterationen, jede weitere brachte weniger Zuwachs als die vorherige. Als jede neue Runde unter 0,1% zusätzliche Coverage produzierte, brachen wir ab. Nach mehreren Stunden Laufzeit und einem enormen Tokenverbrauch stand unterm Strich eine Steigerung von gerade einmal rund 2%, bei gleichzeitig tausenden neu angelegten Tests. Das war zunächst schwer zu erklären.

Die spätere Analyse brachte zwei Ursachen ans Licht. Erstens hatte Claude nicht erkannt, welche Codepfade durch die bestehenden Tests bereits abgedeckt waren. Statt Coverage-Lücken zu schließen, wurden massenhaft Szenarien getestet, die längst abgedeckt waren. Zweitens war der Kontext schlicht zu groß. Eine Codebasis dieser Dimension in einem einzigen Durchlauf abarbeiten zu wollen führt dazu, dass die KI weder das große Bild im Blick behalten noch auf Klassenebene präzise Tests schreiben kann. Beides zusammen führte zu dem ernüchternden Ergebnis.

Fokussiert und iterativ

Aus diesen Fehlern zogen wir die Konsequenzen. Statt die gesamte Projektmappe auf einmal anzugehen, wählten wir ein einzelnes Projekt ohne jegliche bestehende Tests als Startpunkt. Ausgehend von diesem Startpunkt erstellten wir einen Skill mit dem in Claude Code enthaltenen Skill-Builder. Da die bisher hinterlegten Konventionen nicht reichten bzw. nicht ausreichend berücksichtigt wurden, instruierten wir den Skill-Builder, die aktuellen Testprojekte zu analysieren und entsprechende Tests-Konventionen und Muster abzuleiten und direkt im Skill zu hinterlegen. Er fand selbstständig unsere Basisklassen, die verwendeten Frameworks (xUnit, FluentAssertions, NSubstitute), die Teststruktur und die Namenskonventionen. Innerhalb dieses klar abgegrenzten Kontexts verfeinerten wir den Skill, bis das erste Projekt eine gute Testabdeckung erreicht hatte. Das Ergebnis im Vergleich zum ersten Versuch war deutlich besser. Das angestrebte Ziel der Line Coverage von 80% wurde für das einzelne Projekt erreicht.

Was folgte, waren vier intensive Arbeitstage zu dritt und rund 15 Commits, die den Skill schrittweise verbesserten. Nicht durch theoretische Überlegungen im Vorfeld, sondern durch echte Fehler beim echten Einsatz. Jeder Lauf zeigte etwas, das nicht funktionierte. Jede Erkenntnis floss sofort als neue Regel oder Konvention zurück in den Skill.

Prompt:

Extrahiere aus der aktuellen Session alle Erkenntnisse zum Schreiben der Unit Tests um die Testabdeckung zu erreichen. Bewerte die Erkenntnisse nach Relevanz für den Skill. Wenn die Relevanz hoch genug ist, füge die Erkenntnis dem Skill hinzu.

Zum Beispiel, wie die Line Coverage im Projekt gemessen wird und wie Claude herausfinden kann, welche Zeilen noch nicht abgedeckt sind. Der Skill wurde mit jeder Anwendung präziser. Das ist der entscheidende Unterschied zu einem einmaligen Prompt: Ein Skill akkumuliert Wissen. Am Ende hatten wir ein Werkzeug, mit dem wir zuverlässig und reproduzierbar die 80%-Marke für jedes neue Teilprojekt erreichen konnten.

Die .NET-Solution umfasste insgesamt 72 Projekte. Anfangs statteten wir zu dritt die Projekte einzeln nacheinander mit Tests aus, pro Projekt und Person etwa 2 bis 3 Stunden. Dieses zeitaufwendige Vorgehen, bei dem wir die meiste Zeit mit Warten verbrachten, führte schnell dazu, dass wir mehrere Agenten parallel einsetzten.

Parallelbetrieb: 48 Agenten gleichzeitig

Nachdem die ersten Projekte mit dem Skill um Tests erweitert wurden, wollten wir die Testgenerierung über alle Projekte hinweg möglichst effizient abarbeiten. Da Unit Tests von Natur aus unabhängig von anderen Unit Tests sind, kann man dies sehr gut parallelisieren. Die Lösung war der Einsatz von Sub-Agents von Claude Code. Das Prinzip ist, dass es einen Haupt-Agent gibt, der eine bestimmte Anzahl von Sub-Agents orchestriert. Mit unserem Skill instruierten wir den Haupt-Agent, für jede Testklasse einen eigenen Subagent zu erzeugen. Jeder dieser Agenten hatte die Aufgabe, für eine spezifische Klasse Unit Tests zu erstellen. Die Anzahl der parallelen Sub-Agents limitierten wir dabei auf 8. Das brachte einen deutlichen Schub an Geschwindigkeit.

Aus dem Skill:

Beauftrage immer einzelne Agenten mit dem Schreiben von Tests (ein Agent pro Klasse, Modell: "Sonnet"). Weise einem Agenten NICHT mehrere Klassen zu. Starte bis zu 8 Agenten parallel.

Dennoch sahen wir weiteres Potenzial. Die Hardware Ressourcen unserer Notebooks waren kaum ausgelastet, also warum nicht noch mehr herausholen? Anstatt einfach weitere Agenten hinzuzufügen, entschieden wir uns, den Skill selbst zu skalieren.

Die Lösung: Git Worktrees. Jeder unserer drei Entwickler arbeitete gleichzeitig an zwei oder mehr Projekten in separaten Worktrees, sodass sich die Prozesse gegenseitig nicht blockierten. In jedem dieser sechs Worktrees lief der Skill mit bis zu 8 parallelen Agenten. Das macht in der Spitze 48 Agenten, die gleichzeitig Tests schreiben.

Das Ergebnis

Was uns manuell Monate beschäftigt hätte, haben wir zu dritt in vier Arbeitstagen erreicht: die Line Coverage von 58% auf 82% gehoben und dabei über 16.000 neue Tests generiert. Der Weg dorthin war kein einfaches „Prompt rein, Tests raus", sondern ein iterativer Prozess aus Fehlern, Erkenntnissen und kontinuierlicher Verbesserung.

Lessons learned

Kontext gezielt managen

Bei großen Codebasen akkumuliert ein einzelner Agent über viele Iterationen hinweg so viel Kontext, dass die Qualität der Ausgaben spürbar abnimmt. Die Lösung: Pro zu testender Klasse wird ein eigener Subagent gestartet, der nur diese eine Klasse kennt und nach getaner Arbeit wieder beendet wird. Der Hauptagent übernimmt ausschließlich das Controlling, Coverage messen, nächste Zielklasse auswählen, Subagenten starten, Ergebnisse einsammeln. So bleibt jeder Agent fokussiert, der Kontext sauber, und die Testqualität konstant hoch.

Kontrolle der Coverage-Ergebnisse

Damit Claude seine Ergebnisse prüfen und die nächste Iteration sinnvoll planen kann, braucht er präzise Informationen darüber, wo Coverage fehlt. Im ersten Anlauf haben wir Claude die Cobertura-Dateien direkt auswerten lassen, dabei stießen wir auf ein unerwartetes Problem: Der C#-Compiler übersetzt async-Methoden intern in State-Machines, die als eigene Klassen kompiliert werden. Der Report Generator kam damit nicht zurecht und markierte weite Teile des Codes als "not coverable". Claude half uns, ein kleines Skript zu schreiben, das die Cobertura-Dateien vor der Reportgenerierung bereinigt.

Prompt:

Wir haben ein Problem mit unserer Code-Coverage-Messung: In .runsettings ist CompilerGeneratedAttribute als Exclusion konfiguriert. Das führt dazu, dass alle async-Methoden-Bodies im Coverage-Report komplett fehlen — sie werden als untestbar deklariert und tauchen gar nicht auf. Die Coverage-Zahlen sind dadurch künstlich zu hoch, weil async-Code weder als covered noch als uncovered gezählt wird.

Die Ursache: Der C#-Compiler erzeugt für async-Methoden State-Machine-Klassen (z.B. MyClass.d__5) und für Lambdas Display-Klassen (z.B. MyClass.<>c__DisplayClass3_0). Diese tragen das CompilerGeneratedAttribute und werden daher komplett excluded.

Wenn man die Exclusion einfach entfernt, tauchen die Lines zwar im Report auf, aber als separate Klassen-Einträge — nicht unter der eigentlichen Klasse. Das macht den Report unbrauchbar.

Erstelle ein Powershell-Script zur Korrektur der Cobertura-Dateien, damit async-Methoden korrekt in der Coverage ihrer jeweiligen Klasse auftauchen.

Für das Context-Management brauchten wir zusätzlich ein kompaktes Analyse-Tool: Es sollte die generierten Cobertura-Dateien auswerten und pro C#-Datei die aktuelle Coverage sowie die fehlenden Zeilen ausgeben, absteigend nach Abdeckungsgrad sortiert, damit Claude sofort weiß, wo der größte Hebel liegt. Da wir kein passendes Tool fanden, haben wir auch dieses kurzerhand von Claude erstellen lassen.

Prompt:

Erstelle zur besseren Erkennung von Lücken in der Coverage ein Powershell-Script, dass aus den bestehenden Cobertura-Dateien unterhalb des Verzeichnisses /test-results Informationen im folgenden kompakten Beispielformat in die Standardausgabe liefert:

FileCoverageLinesUncovered
Service.cs8.0%154 / 1916133-138, 206-211, ...
Handler.cs16.9%284 / 1682190-196, ...

Die Dateien sollen dabei absteigend nach Anzahl der Uncovered Lines sortiert sein.

Kontrolle der Tests selbst

Nicht alle generierten Tests waren inhaltlich auf Anhieb überzeugend. Tests - seien sie manuell geschrieben oder generiert - nützen wenig, wenn sie zwar die Line Coverage erhöhen, aber keine sinnvollen Prüfungen enthalten. Daher statteten wir den Skill mit weiteren Regeln und Fähigkeiten aus, z. B. der Nutzung von Snapshot Testing via Verify zur inhaltlichen Prüfung von Methodenergebnissen.

Dazu gehört es auch, Claude “untestbaren” Code erkennen und von der Testgenerierung ausschließen zu lassen. Unit-Tests sollten z. B. keine Zugriffe auf Infrastruktur (DB Queries, Netzwerk etc) testen, da dies über integrative Tests erfolgt. Stattdessen sollte untestbarer Code zur späteren Betrachtung protokolliert werden.

Die Wahl des Modells

Zunächst setzten wir auf das Haiku-Modell, um die Tests zu generieren. Es war deutlich günstiger im Token-Verbrauch und schien für den Einstieg ausreichend. Die Ergebnisse waren jedoch nicht zuverlässig genug. Die eingesparten Kosten rechtfertigten den zusätzlichen Korrekturaufwand schlicht nicht. Wir stellten daher auf model: "sonnet" um und der Unterschied war sofort spürbar. Die Qualität und Konsistenz des generierten Codes verbesserte sich deutlich.

Kosten im Blick halten

Das Tempo, mit dem die Tests generiert wurden, war beeindruckend, der Tokenverbrauch leider auch. Am Ende standen rund 1.500 USD allein für API-Kosten. Wer diesen Ansatz in Betracht zieht, sollte das frühzeitig einkalkulieren und abwägen, ob das Verhältnis aus Geschwindigkeit, Qualität und Kosten für das eigene Projekt stimmt.

Sandboxing

Die Nutzung von Claude im Sandboxing-Modus eliminierte die Notwendigkeit ständiger Berechtigungsvergabe. Dies vereinfachte die parallele Arbeit mit mehreren Subagenten erheblich.

Fazit

Die Entwicklung eines dedizierten Skills war der entscheidende Schritt. Er hat uns ermöglicht, ein Coverage-Ziel zu erreichen, das auf manuellem Weg in dieser Zeitspanne schlicht nicht erreichbar gewesen wäre und das reproduzierbar über eine Vielzahl von Teilprojekten hinweg.

Gleichwohl wäre es unehrlich, die Schwächen zu verschweigen:

Die generierten Testnamen sind oft lang und techniklastig (MethodName_WhenCondition_ReturnsSpecificResult) und folgen eng der Logik des zu testenden Codes, der oft ohne Rücksicht auf Lesbarkeit und Testbarkeit implementiert wurde. Nicht, dass Entwickler ohne tiefe Kenntnisse des Systems und entsprechenden Domänenwissens dies unbedingt besser hinbekommen hätten.

Ohne genauere Anleitung neigt Claude dazu, wenig aussagekräftige Tests zu generieren. Oftmals beschränken sich diese Tests darauf, zu überprüfen, ob ein Endpunkt ohne Fehler aufgerufen werden kann. Der dazugehörige negative Test stellt lediglich sicher, dass irgendeine Exception ausgelöst wird, ohne das tatsächliche Ergebnis zu validieren. Solche Tests führen zwar zu einer besseren Coverage auf dem Papier, tragen aber nicht zur echten Qualitätssicherung bei. Um dies zu verhindern, ist es notwendig, Claude mittels präziser Regeln anzuleiten und die generierten Tests zu überprüfen.

Ein großer Teil der Tests ist dennoch sinnvoll und sichert fachliche Logik und Randfälle zuverlässig ab. Um generierte Tests von manuell erstellten unterscheiden zu können, haben wir sie mit einer eigenen Annotation [AiGeneratedTest] versehen. Das erlaubt es, bei späteren Refactorings gezielt zu entscheiden, wie die Tests zu bewerten sind und wie mit ihnen umzugehen ist. Eine kleine Maßnahme mit großem praktischen Nutzen.

Die KI erreichte die angestrebte 80 % Code-Coverage nicht autonom. Manuelle Skripte zur Bereinigung und Lückenidentifizierung waren nötig, was den Automatisierungsgrad von Claude Code relativiert. Der Projekterfolg hing somit weniger von der KI-generierten Testsuite ab, sondern auch vom manuellen Debugging der Infrastruktur und effektivem Context Management.

KI-gestützte Testgenerierung ersetzt kein durchdachtes Testkonzept. Aber als Werkzeug, um in kurzer Zeit eine solide Testbasis für eine umfangreiche Codebasis aufzubauen, hat sie ihren Wert klar unter Beweis gestellt, vorausgesetzt man geht fokussiert und strukturiert vor.

Beitrag teilen

//

Weitere Artikel in diesem Themenbereich

Entdecke spannende weiterführende Themen und lass dich von der codecentric Welt inspirieren.

//
Jetzt für unseren Newsletter anmelden

Alles Wissenswerte auf einen Klick:
Unser Newsletter bietet dir die Möglichkeit, dich ohne großen Aufwand über die aktuellen Themen bei codecentric zu informieren.