Im Artikel Code Retreat verwendete ich die Abkürzung TDD, hier möchte ich sie etwas näher beschreiben.
Ein Hinweis vorab
Dieser Artikel ist bewusst nicht-technisch geschrieben, da ich hier (auch) Nicht-Informatiker als Zielgruppe ansprechen möchte. Denn mit diesem Artikel verfolge ich drei Ziele:
bisheriges Vorgehen
Bisher verlief die Softwareentwicklung oft so, dass erst das Produkt programmiert wurde und man das Produkt erst nach Beendigung der Programmierung testete. In der Regeln erfolgten diese Tests manuell und durch andere Personen als die Programmierer. Fanden die Tester einen Fehler, dann mussten die Entwickler diese mit mehr oder weniger großer Hektik ausbauen, weil man das "fertige Programm" ausliefern wollte und der Veröffentlichungstermin in der Regel nicht nur bereits feststand, sondern auch noch sehr zeitnah war, da man die Projektdauer unterschätzte.
Jede Änderung des Programms - und auch eine Fehlerbehebung ist eine solche Änderung! - birgt jedoch immer die Gefahr, dass dadurch andere Fehler durch sogenannte Seiteneffekte entstehen. Wenn bei erneuten Tests neue Fehler gefunden werden, kann sich jeder vorstellen, dass die Hektik nun noch größer wird.
Wie kann man diese Situation verbessern und warum sollte man TDD einsetzen?
TDD ist die Abkürzung für test driven development, also testgetriebene Entwicklung.
Hinter diesem sperrigen Begriff steckt die Überlegung, dass man sich zuerst überlegen soll, was das fertige Produkt können soll und wie man das gewünschte Verhalten testen kann.
Deshalb werden erst die Tests geschrieben und danach das Programm anhand der Tests entwickelt.
Um Fehler frühzeitig zu finden, sollte man sehr früh mit den Tests beginnen und diese Tests auch so häufig wie möglich wiederholen. Es werden also immer wieder die gleichen Testschritte wiederholt und geprüft, ob das gewünschte Ergebnis eintritt.
Immer wieder das Gleiche zu tun und immer das Ergebnis zu prüfen klingt nach einer typischen Aufgabe, die nach Automatisierung schreit. :-)
Deshalb werden die Tests nach Möglichkeit nicht manuell vorgenommen, sondern ebenfalls programmiert.
Die gängigen Entwicklungsumgebungen für viele Programmiersprachen bieten die Möglichkeit, Testcode zu schreiben, der bestimmte Funktionen des Programms testet.
Um mögliche Fehler genau eingrenzen zu können, bietet es sich an, die Tests möglichst klein zu schreiben. Dies hat außerdem den Vorteil, dass die Tests sehr übersichtlich sind und man deren Fehlerfreiheit leichter prüfen kann.
Denn Testcode ist nicht per Definition fehlerfrei, sondern kann selbstverständlich auch fehlerhaft sein!
Selbstverständlich kann und sollte man das Zusammenspielen von mehreren Funktionen ebenfalls testen. Man ordnet Tests deshalb hierarchisch folgendermaßen ein:
Der Begriff "Unit-Tests" bezeichnet eigentlich nur den Modultest, wird aber oft für alle drei Testarten verwendet, da die mir bekannten Entwicklungsumgebungen nur Unit-Tests kennen. Da "Unit" eine "Einheit" bedeutet, reicht es aus, wenn man einfach die Einheit gedanklich anders fasst - beim Mikrotest betrachtet man die kleine Funktion als Einheit und beim Integrationstest eben das Programm.
Vorgehen nach TDD
Beispiel:
Wir möchten eine Funktion schreiben, die bei Übergabe des Zeichen "2" den Text "Es wurde eine gerade Zahl eingegeben" zurückgibt.
Beispielcode in C#:
public static string Zahlenpruefung(string eingabe)
{
return "Es wurde eine gerade Zahl eingegeben";
}
Viele werden jetzt einwerfen, dass die Funktion ja die Eingabe überhaupt nicht prüft, sondern immer "gerade" zurückgibt! Man müsste doch erst einmal prüfen
Hier ist es wichtig, sich klarzumachen, dass es nur darum geht, dass bei Eingabe von "2" der gewünschte Text zurückkommt. Dieser Text wird also erfolgreich durchlaufen werden und damit ist der Schritt 2 beendet!
Klar ist das Beispiel sehr einfach, aber ich wollte es nicht unnötig komplex machen. Gehe einfach davon aus, dass es in der Realität nicht viel anders, sondern einfach nur viel größer ist.
Du hast hier große Bauchschmerzen weil Du große Probleme siehst? Keine Angst, die werden bald wieder kleiner. ;-)
An diesem Beispiel sieht man, wie einfach die Lösung sein kann. Im Schritt 3 geht es darum, so schnell wie möglich wieder auslieferungsfähig zu sein. Wenn z. B. in der Industrie eine Produktionsstraße stillsteht, kostet jede Minute richtig viel Geld. Sobald das Programm wieder funktioniert und damit die Produktionsstraße wieder läuft, sollte man sich natürlich Gedanken machen, ob dieses Provisorium so bleiben soll.
Wer nun sagt, wir haben keine teure Produktionsstraße, weil wir in einem Startup arbeiten, soll sich folgendes Szenario vorstellen:
Die erste oder nächste Finanzierungsrunde steht an und da gerade Ebbe in der Kasse ist, muss möglichst schnell Geld rein, weshalb beim nächsten Termin alles passen muss. Kurz vor dem Termin, an dem Ihr Euer Produkt präsentiert sollt, fällt Euch ein Fehler oder eine fehlende Funktionalität auf. Jetzt könnt Ihr entweder dafür sorgen, dass Euer Programm zum Termin funktioniert oder Ihr könnt eben nichts zeigen und hofft, dass Ihr trotz Nicht-Präsentation die erhoffte Finanzierung bekommt. Falls Ihr die zweite Option wählt - viel Glück! :-)
Zurück zu den Bauchschmerzen ;-)
An diesem Beispiel sieht man aber auch, dass man sich genau überlegen soll, welche Fälle man abtesten muss und wie schnell man technische Schulden erzeugen kann.
Wird die Funktion nur mit gültigen Werten versorgt (während der Präsentation bei der Finanzierungsrunde), dann funktioniert anscheinend alles.
Um nun auch ungerade Zahlen ausgeben zu können, muss die Funktion nun rechnen können und vorher erst einmal sichergestellt werden, dass auch wirklich eine Zahl übergeben wird. Damit erhöht sich der Aufwand deutlich im Vergleich zum Beispiel, in dem einfach eine Zeichenfolge zurückgegeben wird.
Dies zu erkennen und bei der weiteren Entwicklung zu berücksichtigen, halte ich für extrem wichtig!