Jeder Bug, sein Unit-Test
Meiner Meinung nach ist das hier eine der wichtigsten Best Practices überhaupt, wenn man große Softwareframeworks oder umfangreiche Libraries langfristig warten möchte.
Trotz aller Mühe gilt: Selbst mit intensivem, automatisiertem Testen werdet ihr nie bei null Bugs landen. Ja, ihr werdet deutlich weniger Fehler haben, aber ein paar Bugs werden immer durchrutschen. Kein Mensch und kein Code sind perfekt.
Der entscheidende Punkt ist, wie man mit diesen Fehlern umgeht.
Jedes Mal, wenn ein Bug auftaucht, ist das ein Warnsignal: Irgendwann wurde einmal ein Test Case übersehen. Auch wenn der erste Impuls ist, sofort in den Code zu springen und den Bug zu fixen, lohnt es sich, kurz innezuhalten.
Fragt euch:
- Warum hat dieser Test gefehlt?
- Wie lässt sich die Testabdeckung erweitern, damit dieser Fehler in Zukunft automatisch erkannt wird?
💡Merksatz:
Jeder Bug bekommt seinen eigenen Unit-Test. Wenn du einen Bug entdeckst, schreibe zuerst einen Unit-Test, der fehlschlägt. Danach erst korrigiere den Bug und behalte den Unit-Test für immer, um auch bei Refactoring sicher zu sein.
Folgenden Ablauf kann ich empfehlen:
- Bug reproduzieren: Schreibe zuerst einen Test, der den Fehler eindeutig und isoliert sichtbar macht. Er muss reproduzierbar fehlschlagen, sonst hast du den Bug nicht wirklich erwischt.
- Bug fixen: Passe den Code so an, dass der Test bestanden wird.
- Test behalten: Der Test bleibt für immer im Code. Ab jetzt läuft er bei jedem Build und verhindert, dass dieser Fehler unbemerkt zurückkehrt.
Diese Vorgehensweise hat zwei Vorteile:
- Fehler kehren nicht zurück, weil der Test sie sofort erkennt.
- Du dokumentierst das Problem. Der Test selbst ist der beste Beweis, dass du es verstanden und gelöst hast.
Ich habe es schon oft erlebt: Jahre später taucht ein Problem erneut auf, weil jemand den Code ändert und nicht wusste, dass er damit einen alten Bug reaktiviert. Mit einem Unit-Test wäre das nie passiert.
Kurz gesagt: Ein gefixter Bug ohne Test ist kein Fix, sondern eine Pause.
Gerade wenn du viel mit Legacy-Code oder in großen Frameworks arbeitest, kann dir diese Vorgehensweise die Arbeit erheblich erleichtern.
Teste Verhalten, nicht Implementierung
Ein häufiger Fehler: Man verändert den Code nur, um ihn besser testen zu können – etwa durch zusätzliche Rückgabewerte oder Hilfsvariablen, die für die eigentliche Funktion keinen Mehrwert haben.
Das ist ein Anti-Pattern.
Ein Test soll prüfen, ob eine Funktion korrekt reagiert und nicht wie sie intern aufgebaut ist.
Und schon gar nicht sollte der Test den Code zwingen, ihm etwas „zurückzugeben“, was er eigentlich gar nicht braucht.
Anti-Pattern: Code wird verbogen – nur für den Test
def darf_stornieren_bad_practice(status):
if status == "geliefert":
return False, "bereits geliefert" # zweiter Rückgabewert nur für Tests!
elif status == "offen":
return True, None
elif status == "bezahlt":
return True, None
return False, "unbekannter Status"
➡️ Die Funktion gibt hier zusätzliche Informationen zurück – nicht, weil das System sie braucht, sondern nur, weil der Test sie gern hätte.
So sehen die Tests dann aus (für die maximale Lesbarkeit auch für Einsteiger wurde bewusst auf die Verwendung von @pytest.mark.parametrize
verzichtet):
def test_stornierungserlaubnis_mit_begründung():
erlaubt, grund = darf_stornieren_bad_practice("geliefert")
assert erlaubt is False
assert grund == "bereits geliefert"
erlaubt, grund = darf_stornieren_bad_practice("offen")
assert erlaubt is True
assert grund == None
erlaubt, grund = darf_stornieren_bad_practice("bezahlt")
assert erlaubt is True
assert grund == None
Was daran falsch ist:
- Der Test prüft interne Begründungen. Geprüft werden sollte immer das Verhalten nach außen.
- Die Funktion wird unnötig komplex, obwohl ein einfacher
True
/False
Return-Wert reichen würde. - Der zweite Rückgabewert bringt im echten Code keinen Nutzen, sondern wurde nur für den Test eingeführt.
Besser: Funktion macht genau das, was sie soll
def darf_stornieren(status):
return status in ["offen", "bezahlt"]
Und der Test dazu?
def test_stornierungserlaubnis():
assert darf_stornieren("geliefert") is False
assert darf_stornieren("offen") is True
assert darf_stornieren("bezahlt") is True
➡️ Klar, einfach, korrekt.
Der Test fragt: „Darf man stornieren?“ – nicht: „Warum genau nicht?“
💡Merksatz:
Wenn du den Code umbauen musst, nur damit der Test ihn „versteht“, testest du nicht das Verhalten – du missbrauchst die Implementierung.
Tests sollen dem Code nicht diktieren, was er zurückgeben muss, sondern sollen nur prüfen, ob das Verhalten stimmt. Man sollte das aber nicht damit verwechseln, dass sich dein Code kaum verändert, sobald du mit TDD startest. Im Gegenteil – er wird sich mit der Zeit erheblich verändern. Er wird modularer, leserlicher und deutlich besser wartbar. Künstliche Hilfsvariablen nur zum Testen gehören aber nicht dazu.
Halte Tests klein, unabhängig und eindeutig
Ein guter Unit Test hat drei Eigenschaften:
- Klein: Er testet genau eine Sache.
- Unabhängig: Er hängt nicht vom Zustand anderer Tests ab.
- Eindeutig: Wenn er fehlschlägt, weißt du sofort, warum.
💡Merksatz:
Ein guter Unit Test ist kompakt, steht auf eigenen Beinen und verrät dir sofort, ob alles passt.
Verwende aussagekräftige Testnamen
Ein Testname ist oft das Erste, was du oder ein Teammitglied sehen, wenn ein Test fehlschlägt.
Er ist deine erste – und manchmal einzige – Dokumentation darüber, was dieser Test überhaupt prüft.
Namen wie TestCase17
oder FB_Test_03
sind nichts anderes als Rätsel. Sie zwingen dich, den Code zu öffnen, nur um herauszufinden, worum es überhaupt geht. Das bindet unnötig Zeit und erschwert die Fehlersuche.
Besser: Der Testname sagt dir auf einen Blick:
- Welcher Funktionsbaustein getestet wird
- Was er tun soll
- Unter welcher Bedingung dies gelten soll
Ein mögliches und in TwinCAT/TcUnit oft praktisches Namensschema ist:FB_<BausteinName>_<Aktion>_When<Bedingung>
Das ist aber nur eine Variante. Je nach Team und Projekt können auch andere Muster gut funktionieren – entscheidend ist, dass der Name klar, eindeutig und selbsterklärend ist.
❌ Schlechte Namen (nichtssagend)
TestCase17
FB_Test_03
CheckStatus
Run1
✅ Gute Namen (klar und beschreibend)
FB_ConveyorControl_StartsMotor_WhenStartCommandIsTrue
FB_SafetyDoor_Locks_WhenMachineIsRunning
FB_TemperatureControl_ShutsDownHeating_AboveMaxTemperature
FB_RobotAxis_MovesToHomePosition_WhenResetCommandIssued
FB_EventManager_LogsError_WhenInvalidEventReceived
💡Merksatz:
Ein guter Testname ist wie eine gute Commit-Message: Man versteht sofort, worum es geht – ganz ohne weiteren Kommentar.
Besser anfangen als warten
Einer der größten Fehler, die ich in Teams immer wieder sehe: Man wartet mit automatisierten Tests, bis die perfekte CI/CD-Pipeline steht. Alles soll von Anfang an hochprofessionell sein – mit Jenkins, automatischem Reporting, Merge-Checks, Notifications und allem Drum und Dran. Klingt toll, führt aber in der Praxis oft dazu, dass gar nichts passiert. Perfektion kann zu Stillstand führen (siehe dazu im Blogpost Perfektionismus als Gift). Dabei ist der Weg viel einfacher: Starte sofort mit automatischen Unit-Tests, auch wenn du die Pipeline am Anfang noch manuell auslöst. Das hat nichts mit „manuell testen“ zu tun – die Tests selbst laufen natürlich komplett automatisch. Ob du den Start-Button drückst oder ein PR-Hook das macht, ist im ersten Schritt völlig egal. Warum das so wichtig ist, zeigt eine einfache Rechnung: Nehmen wir eine Library mit 100 Unit-Tests. Wenn diese Tests nicht automatisch durchlaufen, sondern einzeln gestartet werden müssten, und jeder Test inklusive Setup 5 Minuten dauert, bist du bei 500 Minuten – also über acht Stunden – für nur einen Testlauf. Unmöglich im Alltag. Wenn die Tests aber automatisch nacheinander laufen, ist der manuelle Aufwand praktisch null. Du siehst am Ende einfach, ob alles grün ist. Die restliche Perfektion – z. B. dass die Pipeline automatisch bei jedem Merge läuft – kannst du später hinzufügen. Das Entscheidende: Schon dieser erste Schritt spart dir 99 % der Arbeit. Du musst nicht warten, bis alles perfekt ist, um den größten Teil des Nutzens zu bekommen.
💡Merksatz:
Lieber heute mit automatischen Unit-Tests starten und die Pipeline manuell triggern, als monatelang auf die perfekte Automatisierung zu warten.
Schreibe einen Kommentar