Auf den Tag drei Wochen sind vergangen, seit Ralf Westphal mich in Reaktion auf den Blogeintrag Wie viel Sinn machen Unittests? ermuntert hat, mich nochmals intensiv mit diesem Thema auseinanderzusetzen:
Unit Tests […] konsequent und kompetent mal für einen Monat einzusetzen. Mit der Kompetenz mag es da schwierig sein, weil man sich dann eine Veränderung der Handlungs- und Denkgewohnheit selbst beibringen muss... aber das ist zumindest ein Anfang.
Ich bin seinem Rat gefolgt und habe bemerkt, dass die wesentlichen Probleme für mich nicht technischer, sondern gedanklicher Natur waren. Zu viele unbeantwortete Fragen standen im Raum – Fragen, die zunächst einfach erscheinen, deren Beantwortung aber viel Nachdenken voraussetzt.
Man könnte mein Problem als die Herausforderung bezeichnen, ein tragfähiges Konzept für Unittests zu finden, das sich nahtlos in meine bisherige Arbeitsweise integriert, sie zwar erweitert, aber nicht von Grund auf umkrempelt.
Bemerkenswert fand ich, dass die wenigsten Entwickler, die ich mit diesen Fragen konfrontiert habe, ihre Antworten fundiert begründen konnten. Einige haben sich sogar daran gestört, dass ich diese Fragen überhaupt stelle, und haben eher dazu geraten, einfach loszulegen – frei nach dem Motto: TDD ist keine Wissenschaft!
Doch ohne fundierte Antworten auf grundlegende Fragen zu haben, die den eigenen Wissensdurst stillen, verbleibt immer ein ungutes Gefühl – ob das, was man macht, tragfähig für zukünftige Erweiterungen sein wird. Diese zukünftige Tragfähigkeit bedeutet mir sehr viel – anders wäre ich vermutlich auch von Extreme Programming nicht so begeistert.
Außerdem ist die Beantwortung solcher Fragen entscheidend, um klare Regeln und Richtlinien zu haben, auf deren Basis man Entscheidungen treffen kann: Je genauer diese Regeln und Richtlinien formuliert werden, desto größer wird die persönliche Liebe zum Detail, desto besser wird die Accuracy – meines Erachtens eine essenzielle Eigenschaft eines guten Enwicklers.
Die erste dieser Fragen, ob Code zum Zwecke einer besseren Testbarkeit angepasst werden darf, wurde bereits in Auch Kirchen haben Kragsteine diskutiert: Wie so oft ist es Ralf Westphal gelungen, eine anschauliche Analogie zur realen Welt zu finden, die das digitale Problem schlagartig löst.
Die zweite dieser Fragen, wie Unittests sinnvoll und übersichtlich organisiert werden können, wurde inzwischen auch beantwortet: Auch hierzu hat Ralf Westphal einen wesentlichen Teil beigetragen, aber auch Neno Loje, Bernd Marquardt und Peter Bucher haben mein nun gefundenes System beeinflusst.
Die grundlegende Erkenntnis ist zunächst, dass sich eine Testklasse nicht zwingend auf eine zu testende Klasse beziehen muss – dass also nicht zwingend eine 1:1-Beziehung zwischen beiden vorliegt. Statt dessen bezieht sich eine Testklasse auf ein sogenanntes System under test (SUT). Ein solches SUT kann durchaus eine Klasse sein – genausogut kann es aber auch eine einzelne Methode sein.
Die zweite Erkenntnis ist, dass ein Unittest ein SUT immer in einem gegebenen Kontext testet. Bei einer Methode ist das fast immer der Aufruf derselben, bei einer Klasse hingegen kann es verschiedene Ausgangssituationen geben: Ein Kontext wäre das Initialisieren der Klasse, ein weiterer die Arbeit mit der gerade initialisierten Klasse, wieder ein weiterer das Disposen einer bereits verwendeten Klasse.
Da sich in der Regel mehrere Unittests auf einen solchen Kontext beziehen, macht es durchaus Sinn, diese in einer Testklasse zusammenzufassen, deren Name den Kontext beschreibt, und deren Namespace den Platz des SUTs angibt.
Auf diese Art könnten Unittests, die den Aufruf der Methode ToDictionary in der Klasse goloroden.de.Common.ExtensionMethods testen, beispielsweise in der Testklasse
- goloroden.de.Tests.Common.ExtensionMethods.WhenToDictionaryIsCalled
abgelegt werden. Die einzelnen Unittests beschreiben dann mit ihrem Namen innerhalb dieser Klasse nur noch die übergebenen Parameter sowie das erwartete Ergebnis:
- WithNull_AnArgumentNullExceptionIsThrown()
- WithAnEmptyString_AnArgumentExceptionIsThrown()
- …
Der Vorteil dieser Terminologie liegt klar auf der Hand: Schlägt ein Unittest fehl, lassen sich aus Testklasse und –methode auf einen Blick alle relevanten Informationen ablesen: Der Aufruf welcher Codestelle verursacht in welchem Kontext mit welchen Parametern ein Problem?
Zudem fördert diese Terminologie das Prinzip von TDD, pro Unittest nur einen einzelnen Aspekt zu testen – versucht man, mehrere Aspekte in einem Unittest zusammen zu fassen, führt dies unweigerlich zu unhandlichen Methodennamen.
Nachdem ich auf dieser Basis nun eine Reihe von Unittests geschrieben habe, bemerke ich, dass sie mir bereits sehr ans Herz gewachsen sind. Interessant fand ich vor allem, dass ich allein durch das nachträgliche Schreiben von Unittests einen Fehler in einer zehnzeiligen Methode entdeckt habe, der trotz dem häufigen Einsatz dieser Methode in den vergangenen zwölf Monaten nicht aufgefallen ist.
Allein das gute Gefühl, zukünftig Änderungen und Erweiterungen an dieser Methode durchführen zu können, ohne Angst haben zu müssen, diesen Fehler unbemerkt wieder einzubauen, war die vergangenen drei Wochen allemal wert.
Insofern kann ich – auch wenn das Schreiben von Unittests für mich noch nicht intuitiv geschieht – guten Gewissens behaupten, dass Unittests sehr wohl Sinn machen. Dies zugebenermaßen sogar mehr, als ich noch vor vier Wochen gedacht hätte.