Am 13. Oktober 2008 haben Peter Bucher und ich unter dem Titel Noch Fragen, Bucher? Ja, Roden! angekündigt, jeweils zum ersten eines jeden Monats einen Kommentar zu einem vorab gemeinsam gewählten Thema verfassen zu wollen. Bisher sind in dieser Reihe folgende Kommentare erschienen:
Heute, am 1. November 2009, ist es nun wieder so weit, und unser Thema für diesen Monat lautet:
Wie viel Sinn machen Unittests?
So wohl Peter wie auch ich haben uns unabhängig voneinander im Vorfeld unsere Gedanken gemacht, wie wir diesem Thema gegenüberstehen. Außerdem nimmt diesen Monat auch Christian Wenz wieder an unserem Streitgespräch teil.
Peters und Christians Kommentare findet sich zeitgleich in den entsprechenden Blogs, folgend nun mein Kommentar zu diesem Thema:
Unittests sind sinnvoll – nicht ohne Grund wird ihr konsequenter Einsatz empfohlen, unter anderem im Rahmen der Clean Code Developer-Initiative und des Extreme Programmings, von der dediziert auf Tests optimierten Entwicklungsmethode Test-Driven Development (TDD) ganz zu schweigen.
In der Theorie hören sich diese Ansätze zunächst auch sehr vielversprechend an. Schließlich ermöglicht der konsequente, durchgängige Einsatz von Unittests eine gefahrlose Änderung von bestehendem Code.
Unbeabsichtigte Seiteneffekte werden durch die Ausführung der Unittests sofort aufgedeckt und können somit vermieden werden. Zudem können gefundene Bugs unmittelbar durch neu geschriebene Unittests abgesichert werden, so dass sie sich nicht erneut einschleichen können.
So weit die Theorie, die Praxis sieht leider ein wenig anders aus. So leicht Unittests nämlich erklärt sind, so kompliziert ist ihre tatsächliche Umsetzung – vorausgesetzt, man strebt tatsächlich eine 100%ige Abdeckung an.
Problematisch sind in der Regel drei Aspekte:
- Der bestehende objektorientierte Aufbau ist unter OOP- und stilistischen Aspekten sauber umgesetzt, läuft einer guten Testbarkeit allerdings zuwider.
- Es bestehen zahlreiche Abhängigkeiten zu externen Komponenten, die sich nicht oder nur mit sehr viel Aufwand simulieren lassen.
- Es bestehen Abhängigkeiten zur konkreten Laufzeitumgebung, die sich nicht oder nur mit sehr viel Aufwand nachbilden lassen.
Entwickler, die sich an Clean Code Developer orientieren,und die auch ansonsten versuchen, beispielsweise mit Werkzeugen wie FxCop ihren Stil und ihre Codequalität zu verbessen, streben in der Regel ein sauberes und durchdachtes objektorientiertes Design an.
Leider steht manchmal genau ein solches Design der Testbarkeit gegenüber. Häufig müssen beispielsweise im Sinne der Testbarkeit weitere Konstruktoren, zusätzliche Eigenschaften oder Schnittstellen zu einer Klasse hinzugefügt werden – obwohl dies aus einer rein objektorientierten Sicht keinen Sinn ergibt.
Roy Osherove beschreibt in seinem Buch The Art of Unit Testing in dem Abschnitt Overcoming the encapsulation problem dieses Problem:
Some people feel that opening up the design to make it more testable is
a bad thing because it hurts the object-oriented principles the design is
based on. I can wholeheartedly say to those people, “Don’t be silly.”
Leider kann ich mich seiner leichtfertigen Aussage “Don’t be silly” nicht so einfach anschließen – denn ein Unittest ist eben nicht einfach nur ein weiterer Verwender der API. Für den Unittest ist es nämlich im Gegensatz zu realen Entwicklern gleichgültig, ob eine API sinnvoll strukturiert ist oder nicht.
Daher empfinde ich es zumindest als bedenklich, ein durchdachtes, objektorientiertes und sauberes Design zu Gunsten einer besseren Testbarkeit zu ändern, wenn es dafür keine weiteren Gründe gibt.
Neben dem potenziell wenig Unittest-tauglichen objektorientierten Design stellt die Abhängigkeit von externen Komponenten ein weiteres häufiges Problem dar. Zu nennen sind hierbei unter anderem:
- Datenbanken
- Dateisysteme
- Registry
Natürlich können all diese Abhängigkeiten entfernt, umgangen oder verschleiert werden – die Frage ist aber, zu welchem Preis dies geschieht. Es genügt nämlich nicht, Stubs und Mocks einzuführen, statt dessen muss die Umgebung auch nach jedem Test potenziell wieder zurückgesetzt werden: Schließlich soll jeder Test isoliert, das heißt unabhängig von seinen Vorgängern, ausgeführt werden.
Eine Datenbank wieder und wieder zurückzusetzen, ist technisch zwar machbar, kostet allerdings sehr viel Zeit. Damit wird das Ziel, alle Unittests bei jedem Build automatisch auszuführen, ab einem gewissen Punkt unerreichbar: Die zur Ausführung der Unittests benötigte Zeit liegt schlichtweg zu hoch.
Zudem können gar nicht alle externen Abhängigkeiten ohne weiteres aufgelöst werden – vor .NET 3.5 SP1 und der Einführung von System.Web.Abstractions war es zum Beispiel nur eingeschränkt möglich, den HTTP-Kontext einer Webanwendung zu simulieren.
Auch die Abhängigkeit von der Laufzeitumgebung an sich kann problematisch sein. So können sich beispielsweise HTTP-Anforderungen, die an eine Webanwendung gesendet werden, voneinander unterscheiden – je nachdem, welcher Webbrowser auf dem Client eingesetzt wird.
Um Webbrowser-spezifisches Verhalten zu simulieren, müssten die Eigenheiten des jeweiligen Webbrowsers im Unittest nachgebildet werden. Alternativ könnten der entsprechenden gewünschte Webbrowser automatisiert werden – was seinerseits allerdings einen ziemlichen Aufwand nach sich zieht.
Ebenfalls problematisch ist multithreaded Code, sofern bestimmte Konstellationen in den einzelnen Threads getestet werden sollen – da das Umschalten zwischen den Threads vom aktuellen Kontext des Prozessors beziehungsweise des Betriebssystems abhängt, kann sich das Verhalten von Ausführung zu Ausführung unterscheiden.
All diese Beispiele zeigen, dass es unter Umständen sehr aufwändig oder gar unmöglich sein kann, einen Unittest zu schreiben, der unter realitätsnahen Bedingungen ausgeführt wird.
Die Frage ist also nicht, ob eine 100%ige Testabdeckung wünschenswert ist – dies gilt rein von der Theorie her ohne jegliche Zweifel – sondern ob eine 100%ige Testabdeckung mit vernünftigem und vertretbaren Aufwand erreicht werden kann: Diese Frage kann durchaus mit Nein beantwortet werden.
Nichtsdestotrotz gibt es natürlich auch Code, der sich perfekt für Unittests eignet: So fallen beispielsweise sämtliche Algorithmen in diese Kategorie, da diese in der Regel kaum oder gar keine Abhängigkeiten zu anderen Komponenten aufweisen.
Daher ergibt es durchaus Sinn, Unittests zu schreiben – die Frage ist lediglich, wofür. Eine 100%ige Abdeckung mit Unittests erreichen zu können klingt zwar verlockend, der zur Erreichung dieses Ziels notwendige Aufwand lohnt in der Regel jedoch nicht.
Insofern muss im konkreten Kontext abgewogen werden, welche Komponenten als kritisch zu betrachten sind und der Abdeckung durch Unittests bedürfen. Selbst für diese Komponenten ist eine 100%ige Abdeckung noch ein ausgesprochen ehrgeiziges Ziel – daher empfiehlt es sich eher, eine zwar hohe, aber nicht perfekte Abdeckung wie beispielsweise 85% anzustreben.
Wie viel Sinn machen Unittests also nun? Zusammengefasst kann man sagen, dass Unittests – an der richtigen Stelle eingesetzt – durchaus Sinn ergeben, dass diese Stellen aber explizit ausgewählt werden sollten.