Softwareentwicklung: Code-Bruchlandung mit Microsofts Copilot

Dieser Text ist eine Übersetzung. Das Original stammt von dem Entwickler Klaas van Schelven und wurde am 14. Januar 2025 hier veröffentlicht(öffnet im neuen Fenster) .
Viele reden derzeit darüber, wie KI dabei helfen kann, Bugs zu beheben. Ich habe hingegen erlebt, wie KI-unterstütztes Coden mir den am schwersten zu findenden Bug des Jahres 2024 einbrachte.
Statt die Leser auf meine aufregende Debugging-Reise mitzunehmen, rufe ich direkt dazu auf, selbst zu suchen! Hier ist also der Bug, den mir Microsoft Copilot präsentierte, während ich an meinen Import-Statements arbeitete.
from django.test import TestCase as TransactionTestCase
Pythons "import as"
Was haben wir hier? Wer sich mit Python nicht so gut auskennt: Das Schlüsselwort as erlaubt es, der importierten Entität beim Import einen neuen Namen zu geben. Dadurch können Namenskonflikte vermieden werden - oder es ist einfach kürzer.
Hier ein paar sinnvolle Beispiele:
# for brevity / idiomatic use:
import numpy as np
# to avoid naming conflicts / introduce clarity:
from django.test import TestCase as DjangoTestCase
from unittest import TestCase as RegularTestCase
Der Bug oben im Code ist kein sinnvolles Beispiel. Im Gegenteil: Es ist die sinnloseste Nutzung von as , die man sich vorstellen kann.
Denn django.test beinhaltet sehr viele verschiedene Testklassen, unter anderem TestCase und TransactionTestCase , die jeweils einen leicht anderen Sinn haben. Der Code oben importiert nun das eine unter dem Namen des anderen.
Der eigentliche Fehler
In diesem Fall haben die beiden TestCases (wie auch der Name des einen impliziert) eine leicht unterschiedliche Semantik bei den Datenbank-Transaktionen.
- Die Klasse TestCase verpackt jeden Test in eine Transaktion, die sie anschließend zurückrollt - es ist also ein isolierter Test.
- Die Klasse TransactionTestCase hat - was vielleicht angesichts des Namens überraschen mag - kein implizites Transaktionsmanagement; dadurch eignet sie sich hervorragend sowohl für Tests, die vom Datenbank-Transaktionsmanagement abhängen, als auch für Tests des Datenbank-Transaktionsmanagements der Anwendung.
Der Bug besteht nun darin, dass man eigentlich TransactionTestCase nutzen möchte, aber wegen des seltsamen Imports der Standard-TestCase läuft - was dazu führt, dass plötzlich alle Tests fehlschlagen. So war es jedenfalls bei mir.
Zwei Stunden meines Lebens
Auf die genaue Beschreibung all der Überraschungen, die ich in diesen zwei Stunden der Fehlersuche erlebt habe, verzichte ich mal. Ebenso auf eine detaillierte Schilderung des Tests, der mich auf die Spur brachte und was ich tat, um nicht noch mal in die gleiche Falle zu tappen(öffnet im neuen Fenster) .
In aller Kürze: Nachdem ich also festgestellt hatte, dass meine Tests wegen der falscher Datenbank-Transaktionen fehlschlugen, sah ich also erst in meinem eigenen Code nach, dann verdächtigte ich Django, nur um am Ende das Problem vom Anfang zu erkennen.
Warum ich Django im Verdacht hatte? Weil ich mir sicher war, TransactionTestCase zu nutzen. Nur verhielt sich TransactionTestCase eindeutig nicht so wie in der Dokumentation beschrieben. Also dachte ich, es gebe irgendeinen gut versteckten Bug in Django, daher ging ich auch diesen Code Schritt für Schritt durch.
Warum ich so lange gebraucht habe, den Bug zu finden
Wenn man es weiß, scheint das Problem einfach zu finden zu sein. Aber wenn man es nicht weiß, sucht und sucht und sucht man. Warum war das so schwierig?
Erstens habe ich zwar vor meinem Commit getestet, aber die Tests nicht direkt laufen lassen, nachdem Copilot die Zeile eingefügt hatte. Als die Tests also fehlschlugen, hatte ich zwei volle Bildschirme diff-Text vor mir.
Zweitens war die Usage Location des Alias irreführend. Hier ist nur von TransactionTestCase die Rede - und der Kommentar dazu lässt einen noch mehr denken, dass hier die richtige Funktion genutzt wird.
class IngestViewTestCase(TransactionTestCase):
# We use TransactionTestCase because of the following:
#
# > Django's TestCase class wraps each test in a transaction and rolls
# > back that transaction after each test, in order to provide test
# > isolation. This means that no transaction is ever actually committed,
# > thus your on_commit() callbacks will never be run.
# > [..]
# > Another way to overcome the limitation is to use TransactionTestCase
# > instead of TestCase. This will mean your transactions are committed,
# > and the callbacks will run. However [..] significantly slower [..]
Das Alias ließ mich denken, dass TransactionTestCase hier korrekt verwendet wird. Zusammen mit dem detaillierten Kommentar sah ich es also als richtigen Weg an, meine Zeit damit zu verschwenden, tief in Djangos Innenleben zu verschwinden, statt mein Augenmerk auf den Import zu richten.
Ein nichtmenschlicher Fehler
Der eigentliche Grund für den großen Aufwand, den Fehler zu finden, war aber, dass er einfach sehr seltsam ist.
Es hat mich ja tatsächlich zwei Stunden Arbeit gekostet, den Fehler zu finden, obwohl er ganz frisch dazugekommen war. Weil ich noch keinen Commit gemacht hatte und sicher war, dass mit dem Commit davor alles in Ordnung war, hätte ich einfach git diff laufen lassen können, um zu sehen, was sich geändert hat.
Tatsächlich habe ich sowohl git diff als auch git diff --staged laufen lassen - und zwar mehrere Male! Aber wer schaut schon auf die Import-Statements?
Die Import-Statements sind wirklich der allerletzte Ort, an dem man einen neuen Fehler erwarten würde. Denn dort liegt eigentlich nur langweiliger, uninteressanter, unveränderter Code.
Um debuggen zu können, muss man Code verstehen, und um ihn zu verstehen, muss man Annahmen machen. Eine vernünftige Annahme (aus der Vor-LLM-Zeit) wäre: Codezeilen wie die oben gibt es nicht. Denn wer würde so etwas schon schreiben?
Sicher, dass es Copilot war ...?
Ja. Leider habe ich keinen Videobeweis oder ein MITM-Log der Anforderungen an Copilot. Ich kann es also nicht beweisen. Aber auch acht Monate später kann ich den Fehler reproduzieren:
from django.test import Te... # copilot autocomplete finishes this as:
from django.test import TestCase as TransactionTestCase
Da ich weiß, dass unter dem Import-Statement Code mit einigen TransactionTestCase folgt, aber keinem TestCase , kann ich mir ungefähr vorstellen, wie eine Maschine, darauf trainiert, Leerstellen zu füllen, zu so einer Zeile kommt. Das ist auf eine Art vernünftig, je nachdem, wie man vernünftig definiert.
Aber: Es ist nicht üblich, es folgt keinem gängigen Muster, es ist einfach keine gute Idee. Also bleibt für mich nur Copilot als Urheber übrig.
Copilot verursacht Crashs
KI-unterstützte Tools führen neue Arten von Fehlern ein. Erfahrene Entwickler kennen ihre eigenen Fehlerquellen und auch die von anderen, von Junior-Entwicklern beispielsweise.
Mit KI kommt aber eine neue Art von Fehlern dazu. Sie wird zuverlässig Fehler produzieren, die keiner erwartet - wie in dem Statement, das Anlass für den Text war. Verlassen wir uns auf die KI-Unterstützung, werden wir es mit Fehlern zu tun haben, die wir eigentlich nicht erwarten würden.
Sie sind quasi die Marotten der KI und sie bringen neue Ebenen der Unvorhersagbarkeit in unsere Arbeitsprozesse. Für mich bleibt KI eine gute Hilfe, aber es ist wichtig, sich darüber im Klaren zu sein, dass KI diese neuen Bugs schaffen kann.
Und was ist nun mit dem Crash in der Überschrift? Um ehrlich zu sein, gab es keinen Crash, weil ich den Code nicht committet hatte - es ist also mehr ein Witz. Die Metapher war nur einfach zu verlockend.
Der Text wurde von Jennifer Fraczek übersetzt - ohne KI-Hilfe.



