TDD mit GitHub Copilot
von Paul Sobocinski
Bedeutet das Aufkommen von KI-Codierungsassistenten wie GitHub Copilot, dass wir keine Tests benötigen? Wird TDD veraltet sein? Um dies zu beantworten, untersuchen wir zwei Möglichkeiten, wie TDD die Softwareentwicklung unterstützt: die Bereitstellung von gutem Feedback und ein Mittel zum „Teilen und Herrschen“ bei der Lösung von Problemen.
TDD für gutes Feedback
Gutes Feedback ist schnell und präzise. In beiden Fällen gibt es nichts Besseres, als mit einem gut geschriebenen Unit-Test zu beginnen. Keine manuellen Tests, keine Dokumentation, keine Codeüberprüfung und ja, nicht einmal generative KI. Tatsächlich liefern LLMs irrelevante Informationen und sogar halluzinieren. TDD wird insbesondere bei der Verwendung von KI-Codierungsassistenten benötigt. Aus den gleichen Gründen, aus denen wir schnelles und genaues Feedback zu dem von uns geschriebenen Code benötigen, benötigen wir schnelles und genaues Feedback zu dem Code, den unser KI-Codierungsassistent schreibt.
TDD für Teile-und-Herrsche-Probleme
Problemlösung durch „Teile und herrsche“ bedeutet, dass kleinere Probleme früher gelöst werden können als größere. Dies ermöglicht kontinuierliche Integration, Trunk-basierte Entwicklung und letztendlich kontinuierliche Bereitstellung. Aber brauchen wir das alles wirklich, wenn KI-Assistenten das Codieren für uns übernehmen?
Ja. LLMs stellen selten nach einer einzigen Eingabeaufforderung genau die Funktionalität bereit, die wir benötigen. Die iterative Entwicklung wird also noch nicht verschwinden. Außerdem scheinen LLMs „das Denken hervorzurufen“ (siehe verlinkte Studie), wenn sie Probleme schrittweise lösen Aufforderung zur Gedankenkette. LLM-basierte KI-Codierungsassistenten erzielen die beste Leistung, wenn sie Probleme teilen und erobern, und TDD ist die Art und Weise, wie wir dies für die Softwareentwicklung tun.
TDD-Tipps für GitHub Copilot
Bei Thoughtworks nutzen wir seit Anfang des Jahres GitHub Copilot mit TDD. Unser Ziel war es, mit dem Tool zu experimentieren, es zu bewerten und eine Reihe wirksamer Praktiken zu entwickeln.
0. Erste Schritte
Mit einer leeren Testdatei zu beginnen bedeutet nicht, mit einem leeren Kontext zu beginnen. Wir beginnen oft mit einer User Story mit einigen groben Notizen. Außerdem besprechen wir mit unserem Paarungspartner einen Ausgangspunkt.
Das ist alles Kontext, den Copilot erst „sieht“, wenn wir ihn in eine geöffnete Datei einfügen (z. B. oben in unserer Testdatei). Copilot kann mit Tippfehlern, Punktformeln und schlechter Grammatik arbeiten – was auch immer. Aber mit einer leeren Datei funktioniert es nicht.
Einige Beispiele für Startkontexte, die für uns funktioniert haben:
- ASCII-Kunstmodell
- Akzeptanzkriterium
- Leitannahmen wie:
- „Keine GUI erforderlich“
- „Verwenden Sie objektorientierte Programmierung“ (im Vergleich zur funktionalen Programmierung)
Copilot verwendet offene Dateien für den Kontext. Wenn Sie also sowohl die Test- als auch die Implementierungsdatei offen halten (z. B. nebeneinander), wird die Codevervollständigungsfähigkeit von Copilot erheblich verbessert.
1. Rot
Wir beginnen damit, einen beschreibenden Testbeispielnamen zu schreiben. Je aussagekräftiger der Name, desto besser ist die Leistung der Codevervollständigung von Copilot.
Wir finden, dass eine Gegeben-Wann-Dann-Struktur in dreierlei Hinsicht hilfreich ist. Erstens erinnert es uns daran, geschäftlichen Kontext bereitzustellen. Zweitens ermöglicht es Copilot, umfassende und aussagekräftige Benennungsempfehlungen für Testbeispiele bereitzustellen. Drittens zeigt es Copilots „Verständnis“ des Problems aus dem Top-of-File-Kontext (beschrieben im vorherigen Abschnitt).
Wenn wir beispielsweise an Backend-Code arbeiten und Copilot den Namen unseres Testbeispiels durch Code vervollständigt, lautet er: „Angesichts der Tatsache, dass der Benutzer … klickt auf den Kaufen-Button”das sagt uns, dass wir den Top-of-File-Kontext aktualisieren sollten, um Folgendes anzugeben: „Angenommen, keine GUI“ oder, „Diese Testsuite ist mit den API-Endpunkten einer Python-Flask-App verbunden.“.
Weitere „Fallstricke“, auf die Sie achten sollten:
- Copilot kann mehrere Tests gleichzeitig programmieren. Diese Tests sind oft nutzlos (wir löschen sie).
- Wenn wir weitere Tests hinzufügen, vervollständigt Copilot mehrere Zeilen anstelle einer Zeile nach der anderen. Aus den Testnamen werden häufig die richtigen Schritte zum „Anordnen“ und „Handeln“ abgeleitet.
- Hier ist das Problem: Es leitet den richtigen „Assert“-Schritt seltener ab, daher achten wir hier besonders darauf, dass der neue Test dies tut richtig scheitern bevor Sie zur „grünen“ Stufe übergehen.
2. Grün
Jetzt sind wir bereit, dass Copilot uns bei der Implementierung unterstützt. Eine bereits vorhandene, ausdrucksstarke und lesbare Testsuite maximiert das Potenzial von Copilot in diesem Schritt.
Allerdings versäumt es Copilot oft, „kleine Schritte“ zu machen. Wenn Sie beispielsweise eine neue Methode hinzufügen, bedeutet der „kleine Schritt“, einen fest codierten Wert zurückzugeben, der den Test besteht. Bisher ist es uns nicht gelungen, Copilot zu diesem Ansatz zu bewegen.
Verfüllversuche
Anstatt „kleine Schritte“ zu unternehmen, geht Copilot einen Schritt weiter und bietet Funktionen, die zwar oft relevant, aber noch nicht getestet sind. Um dieses Problem zu umgehen, „füllen“ wir die fehlenden Tests auf. Obwohl dies vom Standard-TDD-Ablauf abweicht, haben wir bei unserer Problemumgehung bisher keine ernsthaften Probleme festgestellt.
Löschen und neu generieren
Bei Implementierungscode, der aktualisiert werden muss, besteht die effektivste Möglichkeit, Copilot einzubinden, darin, die Implementierung zu löschen und den Code von Grund auf neu generieren zu lassen. Wenn dies fehlschlägt, kann es hilfreich sein, den Methodeninhalt zu löschen und die Schritt-für-Schritt-Anleitung mithilfe von Codekommentaren aufzuschreiben. Gelingt dies nicht, besteht der beste Weg möglicherweise darin, Copilot einfach vorübergehend auszuschalten und die Lösung manuell zu programmieren.
3. Refaktorieren
Refactoring in TDD bedeutet, inkrementelle Änderungen vorzunehmen, die die Wartbarkeit und Erweiterbarkeit der Codebasis verbessern, und das alles unter Beibehaltung des Verhaltens (und einer funktionierenden Codebasis).
Wir haben festgestellt, dass die Fähigkeiten des Copiloten hierfür begrenzt sind. Betrachten Sie zwei Szenarien:
- „Ich kenne den Refactoring-Schritt, den ich ausprobieren möchte“: IDE-Refactor-Verknüpfungen und Funktionen wie die Multi-Cursor-Auswahl bringen uns schneller ans Ziel als Copilot.
- „Ich weiß nicht, welchen Refactoring-Schritt ich machen soll“: Die Vervollständigung des Copilot-Codes kann uns nicht durch einen Refactor führen. Copilot Chat kann jedoch direkt in der IDE Code-Verbesserungsvorschläge machen. Wir haben mit der Erforschung dieser Funktion begonnen und sehen das Potenzial, nützliche Vorschläge in einem kleinen, lokalisierten Umfang zu machen. Mit größeren Refactoring-Vorschlägen (dh über eine einzelne Methode/Funktion hinaus) hatten wir jedoch noch keinen großen Erfolg.
Manchmal kennen wir den Refactoring-Schritt, aber wir kennen nicht die Syntax, die für seine Ausführung erforderlich ist. Erstellen Sie beispielsweise einen Test-Mock, der es uns ermöglichen würde, eine Abhängigkeit einzufügen. In diesen Situationen kann Copilot dabei helfen, eine Inline-Antwort bereitzustellen, wenn Sie über einen Codekommentar dazu aufgefordert werden. Dies erspart uns den Kontextwechsel zur Dokumentation oder Websuche.
Abschluss
Das gängige Sprichwort „Müll rein, Müll raus“ gilt sowohl für Data Engineering als auch für generative KI und LLMs. Anders ausgedrückt: Hochwertigere Inputs ermöglichen eine bessere Nutzung der Leistungsfähigkeit von LLMs. In unserem Fall sorgt TDD für ein hohes Maß an Codequalität. Diese qualitativ hochwertige Eingabe führt zu einer besseren Copilot-Leistung, als dies sonst möglich wäre.
Wir empfehlen daher die Verwendung von Copilot mit TDD und hoffen, dass Sie die oben genannten Tipps dabei hilfreich finden.
Vielen Dank an das „Ensembling with Copilot“-Team, das bei Thoughtworks Canada gegründet wurde; Sie sind die Hauptquelle der in diesem Memo behandelten Erkenntnisse: Om, Vivian, Nenad, Rishi, Zack, Eren, Janice, Yada, Geet und Matthew.