[SL] Technische Skills für agile Softwareentwicklung: Unit Tests

Welche technischen Skills brauchen Entwickler, damit sie regelmäßig (z.B. alle zwei Wochen) eine etwas bessere Version ausliefern können? Gestern habe ich das Grundproblem beschrieben: diese neue Version muss nicht nur neue Features bringen, sondern auch so gut wie fehlerfrei und intern bestmöglich strukturiert sein.

Damit das gelingen kann, brauchen wir automatisierte Tests. Ohne Automatisierung entdecken wir neu eingebaute Fehler zu spät und vergeuden zuviel Zeit beim manuellen Testen. Außerdem werden strukturelle Verbesserungen / Umbauten im Code (Refactorings) durch neue Fehler stark erschwert. Tests für die kleinsten Einheiten im Code heißen Unit Tests (auch: Microtests), eine kurze Erklärung für Nichtprogrammierer ist bei Unit Tests und Knabbergebäck.

Anhand von Unit Tests möchte ich verdeutlichen:

Agile Softwareentwicklung stellt auch substanzielle Ansprüche an die technischen Fähigkeiten von Softwareentwicklern.

(Mit der Einführung von ein paar neuen Rollen und Zeremonien ist es also nicht getan…)

Als Beispiel liefere ich hier nochmal den Unit Test von gestern, ein kleines Programm, das die Regeln für die Überziehung eines Kontos testen soll:

  1. Richte ein Konto ein mit einem Guthaben von 1000 Euro und einem Überziehungslimit von 2000 Euro.
  2. Versuche, 4000 Euro abzuheben.
  3. Prüfe, dass eine Fehlermeldung kommt “Überziehungslimit erreicht, sie können maximal 3000 Euro abheben.”
  4. Prüfe, dass immer noch 1000 Euro auf dem Konto sind.

Wie gesagt, es sieht erstmal einfach aus. Warum haben dann Entwickler Schwierigkeiten damit? Welche technischen oder sonstigen Skills muss man aufbauen, um Unit Tests möglichst erfolgreich einzusetzen?

Die Disziplin aufbringen, für (fast) jede Änderung des Codes Tests zu schreiben. Ohne diese Disziplin wird das Vertrauen in die Tests gering sein, d.h. man muss nach jeder Änderung doch wieder manuell testen, neue Fehler bleiben unentdeckt usw.

Die Unit Tests sollten also möglichst alles abdecken und sehr zeitnah geschrieben werden. Der beste Weg dazu ist TDD - Test Driven Development. Das bedeutet, vor irgendeiner Änderung schreibt man zuerst den entsprechenden Unit Test. Dieser Test wird ausgeführt und schlägt fehl (“Red” - so wird der Fehlschlag angezeigt), dann programmiert man solange, bis dieser eine Test erfolgreich ist (“Green”). Unterwegs werden immer wieder Unit Tests zu benachbarten Bereichen im Code ausgeführt, so dass man sicher sein kann, dass nicht versehentlich irgendwo neue Fehler eingebaut wurden.

Als letzten Schritt macht man ein Refactoring, so dass der gesamte Code bestmöglich für die aktuellen Anforderungen strukturiert ist. Beim Refactoring hilft es wesentlich, dass jetzt alle Tests erfolgreich durchlaufen (“Grün” sind). Der ganze Zyklus heißt dann “Red - Green - Refactor”. Die Entwicklung mit TDD besteht aus einer Folge solcher schnellen Red-Green-Refactor-Zyklen – viele kleine, sichere Schritte.

Gut testbaren Code schreiben. Tests sollten einfacher zu schreiben sein, als der Code, auf den sie sich beziehen. Das bedeutet u.a. unnötige Abhängigkeiten vermeiden, Business-Logik nicht mit Präsentations-Logik vermischen, und allgemein gutes objektorientiertes Design praktizieren.

Bereiche testen, die schwer zu testen sind. Notorisch schwer zu testen sind User Interfaces, da ist es oft so viel einfacher, das Ergebnis anzuschauen, als einen automatisierten Test zu schreiben. Zumal der Test häufig auch noch angepasst werden muss, sobald die Designer sich was Neues ausdenken. Ein Ausweg besteht darin, den Code so aufzuteilen, dass die wichtigen Teile (z.B. Business-Logik) gut testbar sind, und im schlecht testbaren Rest (z.B. dekoratives User Interface) das Risiko von Fehlern zu minimieren.

Die Tests gut strukturiert halten. Ein kleines Problem bringt gleich Dutzende Tests zum Fehlschlagen? Das liegt oft daran, dass der Test-Code schlecht strukturiert ist, z.B. mit einem verschwenderischen Einsatz von Copy-Paste. Entwickler scheinen manchmal alle guten Grundsätze zu vergessen, wenn es um Test-Code geht.

Einen fehlgeschlagenen Test sofort reparieren. Normalerweise weiß ich, dass alle Tests OK (“Grün”) waren, bevor ich mit meiner Programmieraufgabe anfange. Jedes neue Problem muss also durch meine eigenen Änderungen verursacht sein.

Beispiel: Eben war noch alles OK, dann habe ich diese kleine Änderung gemacht, und jetzt wird plötzlich die Mehrwertsteuer nicht mehr richtig ausgerechnet? Dann kann das Problem ja nur in meiner kleine Änderung liegen.

Wenn ich derart mit der Nase darauf gestoßen werde, fällt es mir meist leicht, den Fehler zu beheben. Viel leichter, als wenn ich nach Wochen einen Bugreport von einem Tester bekomme.

Also ist es wichtig, dass wir fehlschlagende Unit Tests nicht ignorieren, sondern sofort das zugrundeliegende Problem finden und beheben.

Tests schnell halten. Wenn die Tests zu lange brauchen, werden sie immer seltener vom Programmierer ausgeführt, liefern also ihr Feedback immer seltener und verspätet. Der sinkende Nutzen führt im Laufe der Zeit dazu, dass Tests vernachlässigt werden. Ein einzelner Unit Test muss in Sekundenbruchteilen ablaufen, nur dann können wir tausende Tests zu einer Test-Suite zusammenfassen, die das gesamte System abdeckt und trotzdem noch in weniger als ca. 10 Minuten abläuft. Alles über 10 Minuten führt zu wesentlich schlechterem weil weiter verzögertem Feedback – die Leute mögen dann nämlich nicht so lange warten und fangen schon etwas Neues an.

Wenn man nicht gegensteuert, wird die gesamte Test-Suite im Laufe der Zeit immer langsamer: es kommen sowieso mehr Tests dazu, und Tests schnell machen erfordert etwas Mehrarbeit und gewisse Programmier-Techniken. So muss man z.B. Zugriffe auf Datenbanken, Filesysteme und Netzwerke weitgehend vermeiden können, was u.U. ein anderes Design des Codes erfordert.

Mit Code zurecht kommen, der noch keine Tests hat. Viele Software-Systeme aus der Praxis sind nur unzureichend mit Unit Tests ausgestattet. Das wollen wir natürlich verbessern, und so bemühen wir uns, in kleinen Schritten die nötigen Tests hinzuzufügen, sobald wir eine Änderung machen.

Nur: Code ohne Tests ist meist schwerer zu testen. Denn dann ist häufig das, was wir eigentlich isoliert testen wollen, tief verborgen unter Schichten von Logik, die uns gerade nicht interessiert. Beispiel: Das obige Bank-Szenario könnte so programmiert sein, dass ich für eine Abhebung von meinem Konto schon ein eingeloggter Benutzer sein muss, wofür ich wiederum eine Reihe von Registrierungs-Seiten abarbeiten muss. Für einen Unit Test dauert das viel zu lange; außerdem kann der Test aus allen möglichen Gründen fehlschlagen, die nichts mit der Überziehung eines Kontos zu tun haben.

In diesem Fall muss man den Code erstmal testbar machen, d.h. die Logik für die Überziehung des Kontos so herauslösen, dass sie isoliert ansprechbar ist. Hier stoßen wir auf ein Henne-Ei-Problem: das Testbar-machen ist eine größere Änderung, und wir haben keine Unit Tests. Wer sagt uns, dass wir damit nicht etwas anderes kaputt machen? Die Lösung ist auch hier: in kleinen, sicheren Schritten vorgehen. Wie das genau geht, steht u.a. im Buch von Michael Feathers Working Effectively with Legacy Code.


Einige Teams sagen: “Ja, wir schreiben Unit Tests – weil wir es sollen. Aber wir wissen nicht, ob es uns unter dem Strich wirklich was nützt.” Nach meiner Erfahrung liegt das daran, dass sie eine oder mehrere der oben genannten Fähigkeiten nicht in genügendem Maße besitzen oder einsetzen.

Unit Tests sind übrigens Punkt 9 in unserem “Flow Check - 15 Schritte zum besseren Software-Lieferprozess”.

Matthias Berth

Alle Emails