
Wie refaktoriert man Code ohne Tests?
Man erbt 100.000 Zeilen Legacy-Code. Keine Tests. Man will refaktorieren, aber weiß nicht, was kaputtgeht, wenn man etwas anfasst. Um Tests zu schreiben, muss man den Code verstehen, und um den Code zu verstehen, braucht man Dokumentation — die es nicht gibt.
Niemand fasst es an. Es verrottet weiter.
Jeder Legacy-Code weltweit steckt in dieser Sackgasse. 60–80 % des IT-Budgets von Fortune-500-Unternehmen fließen in die Wartung von Legacy-Systemen. 42 % der Entwicklerzeit werden für technische Schulden aufgewendet.
Was, wenn ein LLM die Tests stattdessen schreiben könnte?
Die Probleme, wenn man Tests dem LLM überlässt
Wenn man einem LLM sagt: „Schreib einen Test für diese Funktion", kommt irgendetwas heraus. Es gibt drei Probleme.
Erstens: Man weiß nicht, wo man anfangen soll. Bei 527 Funktionen — der Reihe nach ab Nummer 1? Die wichtigsten zuerst? Es gibt kein Kriterium.
Zweitens: Die Qualität der Tests lässt sich nicht überprüfen. Der vom LLM geschriebene Test ist bestanden. Aber prüft dieser Test tatsächlich das Verhalten der Funktion, oder ist es nur eine leere Hülle, die die Funktion aufruft, ohne ein einziges assert? Man muss jeden Test einzeln lesen, um das herauszufinden.
Drittens: Ohne Feedback bleibt das LLM bei 60–70 % stehen. Allein mit „Teste diese Funktion" erreicht man keine 100 % Branch-Coverage. Man muss dem LLM mitteilen, welche Zweige fehlen, damit es den Rest abdeckt.
Das LLM kann nicht keine Tests schreiben. Das Problem ist, dass es keine Struktur gibt, die dem LLM sagt, was es testen soll und wie gut es getestet hat.
tsma: Eine Test-Schiene, die mit einem Befehl läuft
tsma ist ein CLI-Tool, das alle Funktionen eines Projekts indexiert, den Teststatus erkennt, Coverage misst und dem LLM-Agenten präzises Feedback gibt.
Der Agent muss nur einen Befehl kennen:
$ tsma next
Dieser eine Befehl treibt die gesamte Schleife an:
$ tsma next # Zeigt die nächste Funktion ohne Test
→ Test schreiben
$ tsma next # Erkennt den neuen Test, führt ihn aus, misst Coverage
→ 100%? PASS, weiter zur nächsten Funktion
→ <100%? Zeigt nicht abgedeckte Zweige mit Zeilennummern
$ tsma next # Misst den korrigierten Test erneut
→ Verbessert oder nicht, markiert als DONE und weiter
Wiederholen, bis „All functions complete!" erscheint.
Validiert an 527 Funktionen
tsma wurde an einem realen Go-Projekt mit 527 Funktionen eingesetzt.
| Ergebnis | Anzahl | Anteil |
|---|---|---|
| PASS (100 % Branch-Coverage) | 246 | 46,7 % |
| DONE (Best-Effort) | 281 | 53,3 % |
| TODO (nicht bearbeitet) | 0 | 0 % |
246 Funktionen erreichten 100 % Branch-Coverage. Die übrigen 281 erreichten keine 100 %, aber Tests wurden so weit wie möglich geschrieben.
Warum erreichen manche Funktionen keine 100 %?
Funktionen, die 100 % erreichen — und solche, die es nicht tun
Ob eine Funktion 100 % Branch-Coverage erreichen kann, hängt davon ab, wie sie ihre Abhängigkeiten bezieht.
Interface (mockable) — 100 % erreichbar:
type Handler struct {
svc AuthSvc // interface — durch Mock ersetzbar
}
Im Test injiziert man ein Mock und kann alle Pfade steuern:
svc := mocks.NewMockAuthSvc(ctrl)
svc.EXPECT().Login(...).Return(result, nil) // Erfolgspfad
svc.EXPECT().Login(...).Return(nil, err) // Fehlerpfad
Konkreter Typ (not mockable) — 100 % nicht erreichbar:
type Handler struct {
svc *service.SMSImportService // Struct-Pointer — nicht ersetzbar
}
Die tatsächliche Implementierung läuft mit internen Abhängigkeiten wie Datenbanken oder externen APIs. Man kann keinen bestimmten Fehler auslösen oder ein bestimmtes Ergebnis erzwingen. Zweige, die von solchen Ergebnissen abhängen, sind per Unit-Test nicht erreichbar.
tsma reagiert darauf: Nach dem Feedback zu nicht abgedeckten Zweigen wird ein weiterer Versuch unternommen. Wenn die Zweige trotzdem nicht erreicht werden, wird die Funktion als DONE akzeptiert. Das ist keine Einschränkung des Tools, sondern spiegelt die Testbarkeit des Codes wider. Mit Interfaces (DI) wären 100 % möglich — aber das bedeutet, den Originalcode zu ändern.
Feedback verändert die Tests des LLM dramatisch
Der eigentliche Wert von tsma liegt weder im Indexieren noch in der Coverage-Messung. Er liegt darin, nicht abgedeckte Zweige mit exakten Zeilennummern anzuzeigen.
Ohne Feedback:
"Schreib einen Test für die Funktion ListContracts"
→ LLM testet nur den Happy Path
→ Coverage 60–70 %
Mit Feedback:
"Schreib einen Test für die Funktion ListContracts"
→ Coverage 65 % (11/17)
→ UNCOVERED:
line 41 — if params.Status != nil
line 44 — if params.BuildingId != nil
line 70 — if err != nil (CountSummary)
→ LLM fügt Tests hinzu, die genau diese Zweige abdecken
→ Coverage 100 %
Dasselbe LLM. Der einzige Unterschied ist das Feedback. Drei Zeilen mit Zeilennummern trennen 60 % von 100 %.
Wenn der Agent abstürzt, bleibt der Fortschritt erhalten
LLM-Agenten stürzen ab. Token-Limit, Netzwerkfehler, Sitzungsabbruch. 527 Funktionen in einer einzigen Sitzung abzuarbeiten ist unmöglich.
tsma speichert den Fortschritt persistent in .tsma/session.json.
$ tsma status
527 functions
PASS: 246 (46.7%)
DONE: 281 (53.3%)
TODO: 0 (0.0%)
Wenn der Agent bei der 200. Funktion abstürzt? Ein neuer Agent gibt tsma next ein und macht bei der 201. weiter. session.json ist der Checkpoint.
Mehrere Agenten können abwechselnd arbeiten, ohne Konflikte. Jede Funktion ist eine atomare Einheit.
Die Session ist Cache, die Quelldatei ist die Wahrheit
Ein Designprinzip von tsma: Die Session ist Cache, die Quelldatei ist die Source of Truth.
Wenn man eine Testdatei löscht, wird die Funktion wieder zu TODO — selbst wenn session.json sie als PASS führt. Die Session weicht nie von der Realität ab.
Prinzip:
session.json sagt "PASS"?
Keine Testdatei vorhanden → TODO
Quelldatei geändert → erneute Messung
Anweisungen für den LLM-Agenten
Der Agent braucht 6 Zeilen Anweisung:
1. tsma next ausführen
2. TODO → Funktion lesen und Test schreiben
3. Test schlägt fehl → Fehler lesen und Test korrigieren
4. Nicht abgedeckte Zweige angezeigt → Tests für diese Zweige hinzufügen
5. PASS/DONE → nächste Funktion wird automatisch angezeigt
6. Wiederholen, bis "All functions complete!" erscheint
Der Agent muss nur einen Befehl kennen: tsma next. Den Rest erzwingt das CLI.
Der Zug und die Schienen
Vibe Coding ist ein Zug. Schnell. Aber ohne Schienen entgleist er.
Alle AI-Coding-Tools konzentrieren sich darauf, den Zug schneller zu machen. Größere Modelle, klügere Agenten, bessere Prompts. Aber je schneller der Zug wird, desto größer der Schaden bei einer Entgleisung.
tsma ist die Schiene. Das LLM generiert Tests (Neural), das CLI definiert „bis hierhin und nicht weiter" (Symbolic Constraint). Die Kreativität des LLM bleibt unangetastet, aber die Qualität des Ergebnisses wird maschinell erzwungen.
| Bisher | tsma | |
|---|---|---|
| Tests schreiben | Mensch (langsam) oder LLM (chaotisch) | LLM schreibt, CLI verifiziert |
| Wo anfangen? | Mensch entscheidet | CLI bestimmt die Reihenfolge |
| Qualitätsprüfung | Mensch reviewt | CLI misst Coverage |
| Feedback | Keines | Nicht abgedeckte Zweige mit Zeilennummern |
| Fortschritt | Keiner | session.json automatisch |
Das LLM generiert frei. Aber es fährt nur auf der Schiene von tsma next.
Sprachunterstützung
| Sprache | Indexer | Test-Runner | Coverage |
|---|---|---|---|
| Go | go/ast | go test | go test -coverprofile |
| TypeScript | regex | npx vitest / npx jest | c8 / istanbul |
| Python | regex | pytest | coverage.py |
Go verwendet einen AST-Parser für präzise Funktionsextraktion. TypeScript und Python basieren auf regulären Ausdrücken.
Generierte Dateien (*_gen.go, *.pb.go), Testdateien und vendor/node_modules werden automatisch vom Indexing ausgeschlossen.
Installation und Ausführung
make install
cd your-legacy-project
tsma next
Das ist alles.
MIT License. github.com/park-jun-woo/tsma