Unit-Tests sind spröde: Wenn Sie die zu testende Klasse ändern, besteht eine überdurchschnittlich hohe Wahrscheinlichkeit, dass Sie auch eine ganze Reihe von Unit-Tests ändern müssen. Andererseits helfen Ihnen Unit-Tests dabei, über das Design auf einer Mikroebene nachzudenken. Der Test zeigt, was eine Methode tun soll, ohne Raum für Interpretationsfehler, die entstehen, wenn Sie Abstraktionen als Design verwenden. Sollten wir also Unit-Tests verwenden oder nicht?
Vor ein paar Wochen hatten wir bei einer unserer Wissensaustausch-Sitzungen eine Diskussion über Unit-Tests. Das Problem ist, dass Tests brüchig sind, weil sie stark von dem zu testenden Code abhängen. Wenn Sie den zu testenden Code ändern, ist es sehr wahrscheinlich, dass der Test abbricht. Wenn der Test den zu testenden Code sehr gut kennt, ist das zu erwarten. Wenn Sie das testgetriebene Paradigma auf die Spitze treiben, sollen Unit-Tests nur eine einzige Klasse testen. Die zu testende Klasse darf also von keinem anderen Code abhängen. Eine Klasse, die von allen anderen Codes unabhängig ist, ist eine seltene Sache, also brauchen wir einen Trick, um die Unabhängigkeit in Tests zu gewährleisten. Die Standardlösung besteht darin, alle Klassen, von denen die zu testende Klasse abhängt, zu mocken. Da Mocks und Unit-Tests Annahmen explizit machen, sind sie ein guter Weg, um Systeme zu entwerfen. Der Testcode stellt nicht nur sicher, dass der Geschäftscode funktioniert, sondern er dokumentiert auch den Geschäftscode. Eine Änderung in diesem System kann jedoch zu einer Menge Arbeit führen, die keinen direkten geschäftlichen Nutzen bringt. Die Tests und Mocks werden es nicht in die Produktion schaffen; sie werden nur benötigt, um den echten Code zu erstellen.
Soweit kein Problem, aber lassen Sie uns in die Zeit zurückgehen, als es noch Case Tools und Codegeneratoren gab, kurz bevor der Internet-Hype über uns hereinbrach und uns das meiste vergessen ließ, was wir vorher gelernt hatten. Damals, als das Leben noch einfach war und man nicht von unvorhersehbaren Belastungen und sekundenschnellen Marketingentscheidungen heimgesucht wurde.
Damals, Ende der achtziger Jahre, wurden Systeme hauptsächlich in zwei Phasen entworfen. In der Entwurfsphase wurde das System mit Hilfe von Entity-Relationship-Diagrammen und funktionaler Dekomposition spezifiziert. Die erste Phase endete als Tabellen in einer Datenbank. Die Funktionen wurden entweder in Berichte oder Eingabeformulare umgewandelt. Die Regeln für die Daten wurden als Constraints spezifiziert, die in einem Datenbankdialekt in client- und serverseitigen Code übersetzt wurden. Die Umwandlung war größtenteils ein manueller Prozess mit nur einer Ausnahme: Die Übersetzung eines ERD in ein Datenbankschema ist normalerweise trivial und konnte daher leicht automatisiert werden. Die Definitionen der Eingabemaske spezifizierten die Datenverwendung: was kann ein Benutzer mit Hilfe der Eingabemaske in der Datenbank ändern. Da für die Daten Einschränkungen festgelegt wurden, war es einfach, Code zu generieren, um die Einschränkungen auch in der Eingabemaske zu überprüfen.
Aber das war im Grunde alles, was Sie tun konnten. Codegeneratoren brachten Sie ungefähr auf die Hälfte des Weges und alles andere war Handwerkskunst mit VI oder Emacs. Die Tools wurden im Zeitrahmen von Windows 3.11 und 95 besser, aber noch immer schrieben wir den meisten Code mit einem Texteditor. Die Folge war, dass der ursprüngliche Entwurf mit der Zeit immer weniger wert war: Die Details wurden dem Code hinzugefügt, während der Entwurf ein Stapel Papier in einem Ordner blieb. Unter solchen Umständen ist es kaum von Wert, das Design während der Programmierung zu ändern, und so verstaubt das Design langsam im Regal.
Am Rande bemerkt: Wir haben früher gesagt, dass Designs den Sinn des Systems für die Endbenutzer vermitteln sollten. Sie sollten für den Kunden verständlich sein. Meiner Erfahrung nach ist das nicht der Fall. Als IT-Mitarbeiter leben wir in einem anderen Paralleluniversum. Jede Form der Abstraktion ist für andere Menschen einfach zu viel. Wenn Sie nicht in der IT-Branche arbeiten, können Sie erst sagen, ob das System das tut, was Sie wollen, wenn Sie es sehen, und nicht vorher. Keine noch so große Menge an ERDs oder Klassendiagrammen oder was auch immer wird Ihnen helfen, niemals. Das kann nur ein funktionierendes System.
Zurück zu den Unit-Tests. Die Parallele sollte klar sein: Logischerweise folgt Java-Code den JUnit-Tests wie Oracle Forms den Funktionsdiagrammen. Unit-Tests schaffen es nicht in die Produktion, genau wie ERDs. Wenn Sie einen Unit-Test ändern, muss auch der Code geändert werden. Hier beginnt die Analogie zu zerbrechen: Wenn Sie in den achtziger Jahren das ERD änderten, ging normalerweise nichts kaputt. Schlimmer noch, eine Gruppe von Leuten konnte fröhlich den Code ändern, während eine andere Gruppe von Leuten die Ordner mit den ERDs änderte, und beide lebten fröhlich in Paralleluniversen, bis das System in Betrieb ging.
Unit-Tests als Design sind wie die Erkenntnis aus der Randbemerkung oben: Wenn wir die Endbenutzer sowieso nicht dazu bringen können, uns zu verstehen, warum sollten wir sie und uns selbst mit Abstraktionen belästigen, die niemand benutzt? Wir können genauso gut Testcode schreiben, der zumindest während der Entwicklung nützlich ist.
Die Frage ist, ob es notwendig ist, all diesen Testcode auch dann noch zu verwenden, wenn wir mit dem Teil des Systems fertig sind, den der Testcode prüft. Nach einer Weile wird der Test zu einem Ärgernis: etwas, das Sie ändern müssen, während Sie eigentlich den Code ändern wollen. Ich denke da an faustdicke Ordner voller funktionaler Dekompositionen.
Wie viele Unit-Tests brauchen wir also tatsächlich? Meine Antwort lautet, dass ein Unit-Test für Schnittstellen zu einer Komponente oder einem Modul erforderlich ist. Ein Modul ist definiert als eine Gruppe von 10-15 Klassen, auf die über eine Schnittstelle zugegriffen werden kann.
Verfasst von

Jan Vermeir
Developing software and infrastructure in teams, doing whatever it takes to get stable, safe and efficient systems in production.
Contact



