KI-generierte Tests, die Bugs verbergen: Was wir bei Audits von Vibe-Coded-Codebases finden

#KI-generierte Tests

Gründer & Lead Developer

Experte für Softwareentwicklung und Legacy-Code-Optimierung

LinkedIn

Eine grüne Test-Suite soll etwas bedeuten. Wenn du eine Vibe-Coded-Codebase zum ersten Mal öffnest und vierhundert erfolgreiche Tests siehst, ist dein erster Gedanke Erleichterung. Schaust du genauer hin, verfliegt diese Erleichterung. KI-generierte Tests haben eine typische Fehlersignatur - sie sind syntaktisch plausibel, strukturell vertraut und funktional leer. Sie bestehen, weil das Tooling einen Test-Run protokolliert, nicht weil etwas Echtes verifiziert wurde.

Bei Audits von Vibe-Coded-Codebases sehen wir immer wieder Tests, die Bugs verbergen, statt sie aufzudecken. Wir sehen dieses Muster in fast jeder Codebase, die wir bei Projekten auditieren, die mit einem Sprachmodell am Steuer begonnen haben. Dieser Beitrag katalogisiert, was wir tatsächlich finden - keine theoretischen Bedenken, sondern wiederkehrende Muster aus echten Codebases - und erklärt, warum diese Fehler schwerer zu erkennen sind als klassische Test-Anti-Patterns.

Was KI-generierte Tests von gewöhnlich schlechten Tests unterscheidet

Bevor wir die Fehlermuster katalogisieren, lohnt es sich zu benennen, was dieses Problem ungewöhnlich macht. Schlechte Tests, die von Menschen geschrieben wurden, sind meist auf lesbare Weise schlecht: eine fehlende Assertion, ein Test, der nie an CI angebunden wurde, ein Integrationstest, den jemand deaktiviert hat, weil er flaky wurde. Schlechte Tests von Sprachmodellen sind auf unsichtbare Weise schlecht, weil das Ziel des Modells darin besteht, Output zu erzeugen, der für ein anderes Modell, einen Linter und einen Reviewer, der zügig vorangeht, korrekt aussieht.

Ein menschlicher Entwickler, der eine echte Assertion weglässt, tut das meist wegen Zeitdruck oder Unachtsamkeit. Ein Sprachmodell lässt echte Assertions weg, weil nichts zu behaupten der Weg des geringsten Widerstands ist, wenn das Modell den erwarteten Output der Funktion, die es testet, nicht kennt. Der Test sieht trotzdem wie ein Test aus. Er importiert die Klasse, instanziiert das Objekt, ruft die Methode auf und schreibt am Ende eine expect- oder assert-Zeile. Die Assertion behauptet nur etwas, das immer wahr ist.

Das ist der Kern des Problems: KI-generierte Test-Suites optimieren für oberflächliche Plausibilität statt echter Verifizierung. Du bekommst Tests, die eine PR-Review auf den ersten Blick bestehen würden, aber keinen Regressionsschutz bieten.

Fehlermuster 1: Assertions gegen gemockte Rückgabewerte

Das häufigste Muster, das wir finden, ist ein Test, der eine Abhängigkeit mockt, den Mock so konfiguriert, dass er einen bestimmten Wert zurückgibt, den Code unter Test aufruft und dann assertiert, dass der Rückgabewert mit dem übereinstimmt, was der Mock zurückgeben sollte. Die Assertion ist zirkulär - sie kann nur fehlschlagen, wenn der Mock selbst fehlerhaft ist, was nicht möglich ist, da der Mock vom Test kontrolliert wird.

Hier ist ein vereinfachtes PHP-Beispiel in dem Stil, den wir antreffen:

public function testCalculateOrderTotal(): void
{
    $pricingService = $this->createMock(PricingService::class);
    $pricingService->method('getUnitPrice')->willReturn(49.99);

    $calculator = new OrderCalculator($pricingService);
    $result = $calculator->calculateTotal(quantity: 2);

    $this->assertEquals(99.98, $result);
}

Dieser Test sieht vernünftig aus. Er mockt einen externen Service, was gute Praxis ist. Aber überlege, was er tatsächlich verifiziert: Wenn jemand OrderCalculator::calculateTotal ändert, sodass es mit drei statt zwei multipliziert oder eine Pauschale addiert, fängt dieser Test nichts - solange die Methode getUnitPrice aufruft und mit irgendetwas multipliziert. Der Assertion-Wert wurde vom Modell hartkodiert, um mit dem erwarteten Output der korrekten Implementierung übereinzustimmen, nicht aus der Logik dessen abgeleitet, was der Kalkulator tun soll.

Der echte Test würde Daten aufsetzen, den Kalkulator mit mehreren Mengenwerten durchlaufen und die mathematische Beziehung assertieren - nicht ein einziges hartkodiertes Produkt aus zwei hartkodierten Zahlen.

Fehlermuster 2: Tests, die den relevanten Code-Pfad nie ausführen

Sprachmodelle schreiben Tests für die Funktion, die sie testen sollen. Sie schreiben nicht unbedingt Tests, die die interessanten Pfade durch diese Funktion ausführen. Was wir häufig sehen, ist ein Test, der den Happy Path einmal aufruft, eine bestandene Assertion produziert und jeden bedingten Branch untestet lässt. Coverage-Tools berichten die Datei als abgedeckt. Die Branches, die Bugs enthalten - typischerweise die Error-Handling-Pfade, die Edge Cases und die Szenarien, in denen ein externer Aufruf fehlschlägt - werden nie ausgeführt.

In einem typischen Audit kann ein Projekt 78% Line Coverage zeigen, die sich auf unter 30% Branch Coverage in derselben Codebase reduziert. Der Unterschied besteht fast vollständig aus KI-generierten Tests, die die ersten Zeilen jeder Funktion abdecken, aber zurückkehren, bevor sie die bedingte Logik erreichen.

Das ist in der Produktion bedeutsam, weil die nicht getesteten Branches überproportional die Fehlerpfade sind. Ein Bug im Happy Path taucht normalerweise während der Entwicklung auf. Ein Bug im Error-Handling-Pfad taucht auf, wenn ein echter Nutzer sonntags um 2 Uhr nachts auf einen Edge Case trifft.

Fehlermuster 3: Implementation Mirroring

Ein subtileres Fehlermuster ist das, was wir Implementation Mirroring nennen: Tests, die die Implementierungslogik reproduzieren statt das erwartete Verhalten von außen zu spezifizieren. Der Test und der Code unter Test sind logisch identisch, sodass sie alle dieselben Bugs teilen.

In einem JavaScript-Beispiel:

it('should format currency', () => {
  const amount = 1234.5;
  const result = formatCurrency(amount);
  expect(result).toBe(`$${amount.toFixed(2)}`);
});

Diese Assertion ist in Ordnung, solange die Implementierung korrekt ist - aber das String-Template in der Assertion ist im Wesentlichen dieselbe Logik wie die getestete Funktion. Wenn die Funktion einen Bug hat (vielleicht rundet sie in manchen Locales falsch oder lässt das Dollarzeichen bei negativen Werten weg), missed dieser Test ihn, weil er dieselben Annahmen wie die fehlerhafte Implementierung macht.

Eine echte Verhaltens-Spezifikation würde beschreiben, was der Output für bestimmte Inputs sein soll - abgeleitet aus Produktanforderungen, nicht indem man die Funktion mental ausführt und das Ergebnis aufschreibt.

Fehlermuster 4: Test-Isolation, die bei Integration versagt

Sprachmodelle mocken standardmäßig alles. Das erzeugt Tests, die schnell, deterministisch und bedeutungslos sind. Wenn wir dieselbe Codebase gegen eine echte Datenbank oder einen echten HTTP-Client laufen lassen, erweisen sich bedeutende Anteile dieser Test-Annahmen als falsch.

Wir finden konsistent drei Kategorien von Integrationsfehlern, die durch übermäßig gemockte Unit-Tests verborgen werden. SQL-Queries, die gegen den Mock funktionieren, aber gegen das echte Schema fehlschlagen, weil der Mock keine Constraints oder Spaltentypen korrekt modelliert hat. HTTP-Clients, die Mock-Payloads zurückgeben, die durch die Annahmen des Modells über eine API geformt wurden statt durch das tatsächliche Response-Format der API. Service-Layer-Code, der eine bestimmte Transaktionsgrenze voraussetzt, die der Mock ignoriert.

Die Ironie ist, dass das Mocking oft übermäßig ist, gerade weil das Modell "gut isolierte" Unit-Tests produzieren wollte. Ein Code-Qualitäts-Audit einer Vibe-Coded-Codebase wird fast immer empfehlen, einen bedeutenden Teil der mock-schweren Unit-Suite durch eine kleinere Anzahl von Integrationstests zu ersetzen, die tatsächlich die Verträge zwischen Schichten validieren.

Fehlermuster 5: Test-Namen, die Code statt Verhalten beschreiben

Das ist individuell das am wenigsten schädliche Fehlermuster, aber das aufschlussreichste Signal für KI-Testgenerierung im Großen: Test-Namen wie testCalculateOrderTotal, should_return_value oder it handles the case. Das sind Namen, die einen Aufruf einer Funktion beschreiben statt ein Verhalten, von dem ein Nutzer oder System abhängt.

Gute Test-Namen sind Spezifikationen: invoice_total_includes_tax_when_customer_is_vat_registered, payment_fails_with_insufficient_funds_error, concurrent_updates_do_not_produce_negative_inventory. Wenn ein Test mit einem dieser Namen fehlschlägt, weißt du sofort, was kaputt ist und warum es wichtig ist. Wenn testCalculateOrderTotal fehlschlägt, musst du den Test lesen, um zu verstehen, was er überhaupt geprüft hat.

Test-Namen sind kostenlose Dokumentation. KI-generierte Test-Suites nutzen sie systematisch zu wenig.

Warum Coverage-Zahlen nicht die Antwort sind

Die natürliche Reaktion auf all das ist, den Coverage-Schwellenwert zu erhöhen. Wenn Tests oberflächlich sind, fordere mehr Coverage. Das ist der falsche Instinkt. Coverage misst, welche Code-Zeilen während eines Test-Runs besucht wurden - es sagt nichts darüber aus, was verifiziert wurde. Eine Test-Suite, die jede Zeile besucht, indem jede Funktion mit einem einzigen Input aufgerufen wird und keine Assertions gemacht werden, kann 100% Coverage erreichen.

Die nützlichen Metriken sind Branch Coverage und Mutation Score. Branch Coverage verlangt, dass jede Bedingung in der Codebase in beide Richtungen ausgewertet wurde. Mutation Testing führt Hunderte kleiner automatisierter Änderungen am Quellcode durch - Vergleiche umkehren, Return-Statements entfernen, Operatoren tauschen - und prüft, ob deine Test-Suite jede Änderung erkennt. Wenn eine Mutation unentdeckt bleibt, hast du eine Test-Lücke. Diese Tools sind langsamer und geräuschvoller als Line Coverage, messen aber etwas Echtes.

Bevor du einer KI-generierten Test-Suite vertraust, lasse sie durch ein Mutations-Framework. Für PHP ist Infection das Standard-Tool. Für JavaScript, Stryker. Der Mutations-Score bei einem Vibe-Coded-Projekt landet beim ersten Durchlauf typischerweise zwischen 30-50%. Eine produktionstaugliche Suite sollte über 70% liegen.

Wie eine vertrauenswürdige Test-Strategie aussieht

Nach dem Audit der bestehenden Tests baut ein Rettungs-Engagement die Test-Strategie typischerweise um drei Prinzipien herum neu auf.

Das erste ist Verhalten vor Implementierung. Tests sollten gegen die öffentliche Schnittstelle einer Komponente geschrieben werden, spezifizierend, was die Komponente für das System und seine Nutzer tut, nicht wie sie es tut. Die Implementierung kann refaktoriert werden ohne eine Test-Suite zu berühren, die auf Verhalten basiert; eine Test-Suite, die auf der Implementierung basiert, muss jedes Mal umgeschrieben werden, wenn sich die Interna ändern.

Das zweite ist proportionales Integrationstesten. Nicht alles muss ein isolierter Unit-Test sein. Für Datenbankabfragen, für Drittanbieter-API-Aufrufe, für Datei-I/O: Integrationstests, die das echte System in einer kontrollierten Umgebung treffen, geben pro Zeile Test-Code weit besseren Regressionsschutz als Unit-Tests mit komplexen Mock-Setups. Ein Legacy-Code-Modernisierungs-Engagement schließt fast immer das Ersetzen fragiler Mock-Türme durch schlanke, datenbankgestützte Integrationstests ein, die die Verträge zwischen Schichten validieren.

Das dritte ist explizite Fehlerspezifikation. Jeder Test für Error-Handling-Code sollte verifizieren, dass der richtige Fehler für den richtigen Input produziert wird - nicht nur, dass die Funktion etwas zurückgibt ohne zu werfen. Die Fehlerpfade sind, wo echte Bugs leben.

Eine ehrliche Einschätzung bekommen

Wenn dein Projekt eine CI-Pipeline hat, die besteht, und eine Test-Datei-Anzahl, die gesund aussieht, aber mit wesentlicher KI-Unterstützung gebaut wurde, enthält die Test-Suite fast sicher einen bedeutenden Anteil der hier beschriebenen Fehlermuster. Das ist kein Grund zur Panik - es ist ein Grund für eine strukturierte Prüfung, bevor du einen Skalierungs-Meilenstein, ein Enterprise-Procurement oder eine Investor-Due-Diligence erreichst, die eine technische Prüfung beinhaltet.

Der schnellste Weg zu einer ehrlichen Einschätzung ist, Mutation Testing auf einem repräsentativen Modul laufen zu lassen und den Score anzuschauen. Liegt er unter 50%, hast du ein Coverage-Theater-Problem. Ein Code-Qualitäts-Consulting-Engagement kann eingrenzen, wie ein realistischer Fix aussieht, den Sanierungsaufwand schätzen und die Module priorisieren, bei denen Test-Lücken das größte Produktionsrisiko tragen.

Wenn du besprechen möchtest, was du in deiner eigenen Codebase siehst, melde dich unter hello@wolf-tech.io oder besuche wolf-tech.io für ein Gespräch. 30 Minuten reichen aus, um zu beurteilen, ob du ein Problem hast, das du jetzt lösen solltest, oder eines, das warten kann.