Intuitiv erscheint uns die Reihenfolge ja sinnvoll: erst schreiben wir ein Programm und dann testen wir es. Ergibt das wirklich Sinn?
Warum einem dabei ganz schön schwindelig werden kann, kannst du hier nachlesen. Genauso die Methoden, wie du das Problem beseitigen oder umgehen kannst. Inklusive eines kleinen Ausflugs in die Welt unserer Kaffeemaschine.
Erst bauen, dann testen, oder … ?
Bevor wir uns ans Coden machen, betrachten wir doch einmal ein Beispiel aus der realen Welt. In der Küche steht der wohl wichtigste Mitarbeiter in unserer IT-Firma: die Kaffeemaschine.
Der Entwicklung dieses Gerätes ging wahrscheinlich eine Spezifikation voraus, in der beispielsweise Kriterien der folgenden Art beschrieben waren:
- die Maschine soll Kaffeebohnen mahlen und das Pulver brühen können
- für bestimmte Kaffeesorten soll Milch aufgeschäumt werden, die in einem geeigneten Behältnis bereitsteht
- die Programmsteuerung soll verschiedene Kaffeespezialitäten beherrschen
Was dabei herauskommt, kann bisweilen gruselig aussehen:
- Wenn man Wasser nachfüllen möchte, muss man umständlich mit einem Messbecher hantieren oder die halbe Maschine auseinanderbauen
- der integrierte Milchtank fängt nach einer Woche an zu schimmeln
- die Knöpfe sind an der Seite angebracht, so dass das Gerät viel Platz benötigt
- um das Display lesen zu können, muss man sich bücken
Zischt, dampft und kann Latte Macchiato: wurde ordentlich getestet, kommt auch guter Kaffee raus
Vielleicht: erst nachdenken, dann bauen. Oder: Wir sollten den zweiten Schritt nicht vor dem ersten tun.
Wir sehen: es ergibt sogar bei einer Kaffeemaschine Sinn, mit dem Entwurf aus der Sicht der Benutzer anzufangen. Die Funktion muss sich dem Design unterordnen. Die Clientsicht definiert das erwartete Ergebnis, ohne die Inhalte der Umsetzung vorwegzunehmen.
Compile Coffee to Code
Übertragen wir das auf die Softwareentwicklung, kann die Clientsicht sich auf eine einzelne Funktion in der Geschäftslogik beziehen. In den meisten Fällen können wir sagen, welche Eingaben zu welchen Ausgaben führen sollen.
Können wir denn in der Softwareentwicklung erst die Tests schreiben, und dann das Programm? Na klar! Und das nennt sich dann Testgetriebene Entwicklung (TDD):
- erst schreiben wir einen Test – weil es das darunterliegende Programm noch nicht gibt, erhalten wir zunächst einen Kompilierfehler
- dann fügen wir zunächst einmal soviel Programmcode ein, dass wir wieder kompilieren können – normalerweise sollte unser Testfall jetzt rot sein
- wir refaktorieren und testen jetzt so lange, bis unser Test grün wird
- wenn wir Wert auf Clean Code legen, refaktorieren wir jetzt noch einmal den fertigen Code
- dann schreiben wir den nächsten Test
Wichtig ist, dass wir uns nicht gleich den finalen Testfall überlegen, sondern in kleinen Schritten zur Lösung finden. Zunächst testen wir die grundlegende Idee und Funktionalität. Die Prüfung auf Vorhandensein der Funktion ist ein guter Anfang. Dann schreiben wir einen Test für das erste Feature, machen den Test grün, etc. Das gibt uns enorme Sicherheit, weil wir die bisherigen Erkenntnisse immer schon durch Tests abgesichert haben. Außerdem verheddern wir uns nicht in großen theoretischen Gedankengebilden, weil wir versuchen, die Theorie im Kopf herzuleiten. Dadurch, dass wir unser Programm Stück für Stück aufbauen, können wir jederzeit aufhören und an das vorherige Ergebnis anknüpfen. Das formal geforderte Kriterium „Testabdeckung“ wird so nebenbei auch erfüllt. Die Tests können auch nicht mehr aus Zeitgründen weggelassen werden, denn sie sind ja schon da.
Na endlich!
Mit den Tests anzufangen, kann sogar richtig Spaß machen, wie das folgende Beispiel zeigt: Hausautomatisierung ist ja aktuell ein spannendes Thema. Entwickelt werden soll daher eine Heizungssteuerung:
- der Sensor liefert in bestimmten Abständen Temperaturdaten
- sinkt die Temperatur unter 17 Grad, soll die Heizung anfangen zu heizen
- steigt die Temperatur über 22 Grad, soll die Heizung sich abschalten
Das ist noch recht simpel und ließe sich leicht herunterprogrammieren. Wie wir aber etwas später sehen werden, ist das nicht unbedingt eine gute Idee.
Versuchen wir es also einmal andersherum. Schreibe dir einen Test für die erste Bedingung:
@Test public void testTemperatureTooLow() { assertTrue(isTemperatureTooLow(16.9f)); }
Wenn es die zu testende Methode isTemperatureTooLow()
noch nicht gibt, dann implementiere sie einfach:
public boolean isTemperatureTooLow(int temperature) { return true; }
Der Test ist nun grün. Das war schon mal ganz gut, kann aber noch nicht viel.
Schreibe daher einen weiteren Test:
@Test public void testTemperatureOk() { assertFalse(isTemperatureTooLow(17.0f)); }
Das ist dann erstmal wieder rot. Klar, weil noch die Bedingung fehlt. So kriegst du den Test grün:
public boolean isTemperatureTooLow(float temperature) { return temperature < 17f; }
Damit ist die erste Methode schon mal fertig. Nun kannst du einen Testfall schreiben, der für den Fall, dass die Temperatur zu niedrig wird, die Heizung einschaltet. Wenn wir in ganz kleinen Schritten vorgehen, testen wir zunächst einmal, ob das Heizungs-Objekt überhaupt erzeugt wird:
@Test public void switchHeatingOnWhenTemperatureTooLow() { Heating heating = new Heating(); assertTrue(heating != null); }
Damit können wir ermitteln, ob das Objekt fehlerfrei erzeugt wird.
Nun fragen wir etwas Logik ab. Die Heizung wird mit einer Temperatur benachrichtigt und soll sich gemäß ihrer Logik einschalten:
@Test public void switchHeatingOnWhenTemperatureTooLow() { Heating heating = new Heating(); heating.notifyTemperature(16.9f); assertTrue(heating.isOn()); }
Daraus kann diese erste Implementierung folgen:
class Heating { public Heating() { } public void notifyTemperature(float temp) { } public boolean isOn() { return false; } }
Den Test grün zu machen, überlasse ich dir nun selbst. Funktioniert die Methode, solltest du den funktionierenden Code refaktorieren, denn das, was mit dieser Methode herausgekommen ist, lässt sich meistens noch schöner schreiben.
Läuft das, formulierst du die nächste Funktionalität als Testfall. Dann wieder testen, reparieren und refaktorieren. Nach und nach wächst so die komplette Lösung heran.
Was? Wie?… Häää?!?!
Warum haben wir dieses einfache Beispiel jetzt so kompliziert aufgezogen? Na, stell dir einfach mal vor, dass Anforderungen hinzu kommen. Beispielsweise soll die Heizungssteuerung in einer späteren Version auch die Heizung abschalten können, wenn ein Fenster geöffnet wird:
- Fenster wird geöffnet: tritt bei eingeschalteter Heizung ein spürbarer Temperaturrückgang ein, soll die Heizung sich abschalten
- Fenster ist offen: tritt bei ausgeschalteter Heizung ein spürbarer Temperaturrückgang ein, soll die Heizung sich nicht einschalten
- Fenster wird geschlossen: ist der Temperaturrückgang beendet und die Temperatur stagniert unter 17 Grad, soll die Heizung sich einschalten
Das sind noch relativ einfache Bedingungen. Wenn du die Muße hast, modelliere sie einmal auf Papier oder in deiner Entwicklungsumgebung. Gar nicht so einfach, oder? Ich vermute mal, dass du dabei, gerade wenn es schnell gehen soll, ein beachtliches Gebirge aus if-then-else-Verzweigungen aufbauen wirst. Im zweiten Schritt wirst du diese vereinfachen und dich im dritten Schritt am Kopf kratzen und fragen, ob das alles so stimmt. Dabei kann einem ganz schön schwindelig werden, oder?
Gehst du dagegen testgetrieben vor, kannst du dir das Problem schrittweise erarbeiten. Und siehst dabei vielleicht noch Aspekte, die du sonst vergessen hättest. Die Zwischenstände sind dabei immer bereits brauchbar. Und das hat sogar noch einen weiteren Vorteil:
Das… äähhh… läuft!
Stell‘ dir vor, dein Chef kommt rein und erkundigt sich nach dem Stand. Normalerweise würdest du jetzt antworten: „Ich bin dabei, habe aber gerade den kompletten Code auseinandergenommen und kann deswegen nichts zeigen. Ist aber kein Problem, ich habe alles unter Kontrolle.“
Arbeitest du hingegen testgetrieben, kannst du ganz entspannt sagen: „Das Ein- und Ausschalten geht schon mal, und als nächstes mache ich mich an die Fensteröffnungserkennung.“ Hört sich schon deutlich besser an, findest du nicht auch?
Gar nicht mal so schlecht, oder?
Wenn wir dein Interesse an TDD wecken konnten, dann empfehlen wir dir, es bei nächster Gelegenheit einfach mal selber auszuprobieren. Ein Tipp am Rande: lasse dich nicht von den Kommentaren anderer irritieren, falls deine Methodik länger dauern sollte oder seltsam anmuten sollte. Die hohe Qualität deines Quellcodes wird dir recht geben.
Artikel und Beispiele zu testgetriebener Entwicklung findest du hier:
http://www.frankwestphal.de/TestgetriebeneEntwicklung.html
http://www.linux-magazin.de/Ausgaben/2012/08/TDD/(offset)/2
Und natürlich wollen wir dir die aktuelle Diskussion „Is TDD Dead?“ nicht vorenthalten (wir bei Holisticon finden aber nicht, dass dem so ist):
http://david.heinemeierhansson.com/2014/tdd-is-dead-long-live-testing.html
Kommen wir zum Schluss….
Hier endet der Beitrag. Wir hoffen, dass es dir Spaß gemacht hat, den Artikel zu lesen und du einiges gelernt hast.
Fun Fact am Rande: Unsere Kaffeemaschine war kürzlich übrigens kaputt. Ob es am Testen liegt, wissen wir leider nicht.
Zum Glück haben wir dran gedacht, alle geschäftskritischen Ressourcen doppelt auszulegen. Etwas unbequem, aber läuft…