Eine testbare Unit muss der Entwickler halbwegs isoliert benutzen können, sie muss über eine klare Schnittstelle verfügen. Schreibt der Entwickler von Anfang an Unittests, erzwingt er ein solches Design und fördert damit nebenher auch noch die Wiederverwendbarkeit und Modularität seines Codes. Erstreckt sich ein Feature oder Algorithmus über verschiedene Codepassagen oder gar über mehrere Ebenen vom Userinterface bis hin zur Persistence-Schicht, eignet er sich nicht für Unittests.
Ein weiteres Problem besteht darin, die äußeren Abhängigkeiten zu isolieren und zu simulieren. Zieht der Entwickler diesen Schritt nicht von vornherein mit in Betracht, lässt sich das hinterher kaum noch integrieren. Eine Möglichkeit hierfür demonstriert Listing 3. Es zeigt, wie der Entwickler ein Interface herauszieht und eine testspezifische abgeleitete Klasse implementiert. Dafür muss der Code ein Interface oder Ähnliches vorsehen und der Programmierer soll die konkrete Implementierung von außen setzen können. In Listing 3 erledigt das der Konstruktor.
Wichtig ist auch, die nicht testbaren Anteile (also die Abhängigkeiten von der Umwelt) möglichst gut zu isolieren und den entsprechenden Code möglichst klein zu halten. GUI-Code lässt sich in einem Unittest nicht überprüfen, da er eine Benutzerinteraktion erfordert. Im Idealfall sollte der Entwickler solchen Code nur noch deklarativ an den Rest des Programms anbinden.
All diese Dinge zusammen ergeben ein testbares Design. Ohne dies kann ein Entwickler Unittests kaum umsetzen. Es empfiehlt sich, Tests von Anfang an mitzudenken und mitzuschreiben.
Das funktioniert natürlich nur bei neu gestarteten Projekten. In den meisten Fällen existiert das Projekt aber bereits – dann leider oft ohne Unittests. Wer in dieser Situation trotzdem umsatteln möchte, dem sei das Buch “Effektives Arbeiten mit Legacy Code” von Michael C. Feathers empfohlen.
Der Aufwand
Die vielleicht größte Herausforderung ist aber der mit dem Schreiben von Unittests verbundene zeitliche Aufwand. Schon beim weiter oben beschriebenen einfachen Fakultätsbeispiel zeigt der Einsatz von Catch, dass der Unittest-Code mehr Platz einnimmt als die eigentliche Implementierung. Und das ändert sich häufig nicht. Tatsächlich hat der Autor in einigen Fällen mehr als zehnmal so viel Testcode geschrieben wie Code für die eigentliche Implementierung. Zugegeben hat er auch einen Hang dazu, es mit Tests zu übertreiben.
Doch selbst wenn es nur dieselbe Menge an Code ist, muss ein Programmierer ihn schreiben. Und er muss sich vorher Gedanken über normale Testfälle, Grenzfälle, Sonderfälle, Fehlerfälle und vieles mehr machen. Das ist eine Menge trockener Arbeit. Sicher spart der Entwickler hinterher Aufwand beim manuellen Testen, hat ständig alle Testfälle automatisch ausführbar und so weiter. Aber dennoch scheuen viele Entwickler vor Testcode zurück oder vergessen das Testen in der Zeitplanung.
Ein weiteres wichtiges Thema ist das Mocking. Um die passende Umgebung zu simulieren, benötigt der Programmierer Stellvertreter-Elemente in Form von Fakes, Stubs oder Mocks.
Fakes
Fakes sind Funktionen oder Klassen, um die benötigte Schnittstelle zu adressieren, damit der Code übersetzt. Sie verfügen selbst über keine weitere Funktionalität und geben nur Standardwerte zurück. Reine Fakes kommen eigentlich nur selten zum Zug – in der Praxis finden sich viel häufiger Stubs.
Stubs
Dabei handelt es sich um aktivere Stellvertreter-Elemente. Sie geben zum Beispiel spezielle Testwerte zurück, die sich auch mal von Aufruf zu Aufruf unterscheiden. Auf diese Weise ließe sich etwa ein Sensor simulieren, der sicher nicht immer Null zurückgibt. Oder sie reagieren zum Beispiel auf die übergebenen Parameter mit unterschiedlichen Rückgaben oder werfen zum Beispiel in manchen Situationen Exceptions. So ist etwa die Elementfunktion »getName()« im Listing 3 oben ein sehr einfacher Stub, da sie den testspezifischen Wert »Albert Einstein« zurückliefert.





