Notes about Steven Freeman’s Sustainable Test-Driven Development

image

Und wieder habe ich eines meiner Watching-Todos durch und wie kann es anders sein ging es ums Testen, konkret um TDD. Steve Freeman ist jemand den man in der TDD-Welt unbedingt kennen muss. Sein neuerstes Werk “Growing Object-Oriented Software Guided by Tests” steht ganz oben auf meiner To-Read Liste. Auf infoq gibt es bereits jede Menge ähnlicher Präsentationen die sich mit Themen des Buchs auseinandersetzen. Die Schwerpunkte liegen natürlich auf TDD, Mocking und dem Schreiben von sauberen Code.

In dieser Präsentation gibt Steven Freeman Tipps wie man seine Tests auch über längere Zeit sauber (=wartbar, flexibel, leserlich, etc.) hält. Dabei zeigt er an zahlreichen Code-Beispielen, wie Tests absolut selbstbeschreibend implementiert werden können. Um dies zu erreichen, kommt das Mocking-Framework JMock fast in jedem Beispiel zum Einsatz. Dieses Framework besticht durch seine auffällige, eher ungewöhnliche, jedoch ausgesprochen gut lesbare Form. Wenn man das Dev-Team des Test-Helferchens betrachtet, fällt sofort auf, dass Steve Freeman und Nat Pryce (Co-Autor des Buchs) auch mit dabei sind.

Wie bei vielen Projekten wächst nicht nur die Production-Code-Base sondern auch die Tests und natürlich kann es da zu den selben Problemen kommen wie beim eigentlichen Produktionscode. Steven Freeman berichtet von Teams deren Tests so komplex und unwartbar geworden sind, sodass entweder Änderungen nur mehr sehr schwer möglich waren bzw. die entsprechenden Tests überhaupt nicht mehr ausgeführt wurden, was dazu führt, dass man die negativen Seiten beider Welten in seine Code-Base einführt. Natürlich fragt sich das Management, warum das Team Ewigkeiten zum Aufräumen des Codes benötigen, was das Team natürlich unter Druck setzt wodurch Hacks entstehen, wodurch Code dahin-rottet. Wie kann man diese Gegebenheiten vermeiden? Wie kann man Tests auch über längere Zeit sauber halten?

Lesbarkeit von Tests

Der Talk dreht sich hauptsächlich um die Verbesserung von Lesbarkeit und Ausdrucksstärke von Tests. Dabei stellt sich die Frage, wie es mir meine eingesetzte Programmiersprache ermöglicht, Code selbstbeschreibend zu implementieren. Eines ist ganz klar, Kommunikation ist das Ein und Alles wenn es um Lesen von Code geht und wie wir alle wissen, wird immer mehr Code gelesen als tatsächlich geschrieben. Deshalb ist die Aussagekraft von Testklassen extrem wichtig, um den tatsächlichen Produktionscode zu verstehen.

Testnamen beschreiben Features nicht Methoden!

Wie bezeichne ich also dementsprechend meine Testklassen bzw. meine Testmethoden? Steven Freeman erklärt, wenn man Testmethoden nach den Methoden der zu testenden Klasse benennt, dann testet man die Implementierung nicht die Features und deshalb sollte man das nicht tun. Er rät dazu Features zu fokussieren und daher auch so seine Bezeichnungen entsprechend zu wählen.

image

Vorschriftsmäßige Test-Struktur:

Ok, jetzt wissen wir, wie man Tests benennen sollte. Wie geht es weiter? Wie strukturiert man Tests am saubersten? Und wieder ein Rat des TDD-Experten: Das Stichwort ist Wiedererkennungswert! Man sollte Tests so oft es geht möglichst gleich strukturieren. Zum Beispiel implementiert man in seiner Testmethode folgendes Schema:

  1. Setup
  2. Execute
  3. Assert
  4. Teardown

Interessant dabei ist, dass Steven Freeman meistens mit der Assertion beginnt um festzuhalten, was er überhaupt zu Testen versucht. Execute führt die Methode des zu testenden Objekts aus und im Setup-Teil baut man sich das Objektgeflecht zusammen, dass man zur Ausführung des Produktionscodes benötigt.  Das kann natürlich zu einer großen Menge an Code führen was uns schon zur nächsten Freeman’schen Heuristik bringt:

Rationalisieren des Test-Codes:

Mit Hilfe von vielen Hilfsmethoden versucht Freeman die Testinfrastruktur zu beherbergen und den Test kompakt und gleichzeitig lesbar zu gestalten. Dabei erwähnt er immer die Wichtigkeit der Ausdrucksstärke der Tests. Folgendes Beispiel demonstriert den Einsatz so einer Hilfsmethode:

image

Freeman erklärt, dass in diesem Beispiel Struktur verwendet wird um die Absicht des Autors auszudrücken. Wie ich finde, ein ausgesprochen leserlicher Test.

Assertions und Erwartungen eingrenzen:

Auch die Wichtigkeit der Präzision darüber, was bzw. wie viel Erwartet und getestet wird. Das folgende Beispiel zeigt eine Erwartung in JMock wobei die hervorgehobene any-Angabe die Aussage darüber macht, dass die Art der Exception in diesem Fall keine Rolle spielt. Das erhöht natürlich die Flexibilität im Produktionscode. Natürlich könnte die Art eventuell in einem anderen Testfall wichtig sein, jedoch in diesem nicht.

image

Selbstbeschreibende Variablen:

Dabei handelt es sich um keine Überraschung. Konstanten sollten entsprechend bezeichnet werden, um die Absicht des Testfalls auch lange nach der eigentlichen Implementierung verstehen zu können. Dabei kann null z.B. einer gewissen Rolle zugeordnet werden, was die Absicht des Autors natürlich besser auszudrückt als einfach nur null als Parameter zu übergeben. Und wieder: Es geht hauptsächlich um Lesbarkeit und Ausdrucksstärke.

Ok. Bis hier haben wir selbstbeschreibende, gut strukturierte Tests, die die Intention des Autors gut zum Ausdruck bringt. Was aber, wenn ich komplexe Objektgraphen in meiner Code-Base implementiert habe, die ich in meinem Setup-Teil irgendwie strukturiert unterbringen möchte?

Erzeugen komplexer Testdaten:

Probleme mit Objektstrukturen:

Stichwort “Objektmutter” ( = ein Objekt, dass den Objektgraphen aufbaut). Dieses bekannte Muster ist nicht gerade erst entdeckt worden. Wie man im Beispiel gut sehen kann, besitzen wir eine etwas aufwendigere Objektstruktur bezüglich des Order-Objekts, die wir irgendwie in unserem Testfall benötigen. Die sogenannte Objektmutter kapselt genau diese Erzeugung und gibt ihr die entsprechende Bezeichnung. So bleibt das Setup für Testfälle übersichtlich und lesbar.

image

Freeman erwähnt das Problem, das viele Teams nur eine Objektmutter implementieren was natürlich zu einem Abhängigkeitsmagnet mutiert. Deshalb skaliert dieses Muster nur im kleinen Rahmen. Was machen Informatiker daher? Ja genau, wir führen eine zusätzliche Ebene der Indirektion ein und bekommen dann sogenannte Test Data Builder:

Test Data Builder:

Dabei handelt es sich um ein Objekt das Entscheidungen darüber sammelt, was man gerne tun möchte und verpackt diese in gut benannte Methoden. Damit kann man auf elegante Weise den Objektgraphen zusammenbauen, den man für einen bestimmten Test benötigt.

image

Und so kommt der Builder z.B. zum Einsatz:

image

Wie man sehr gut anhand des Beispiels sehen kann, muss man nur die Dinge hinzufügen die wirklich relevant sind. Hier kann man auch gut erkennen, dass auch Dinge die man nicht benötigt (Adresse ohne Postleitzahl) dadurch gut lesbar werden (besser als new Address(‘Sesamstrasse 32/2/2’, null)).

Aufgrund des verwendeten “Fluent” (=return this) Styles, werden eventuelle Fehler, die im nachhinein schwer zu finden sind, relativ leicht auffindbar. Im folgenden Beispiel sieht man sofort, dass London mit Sicherheit ein falscher Wert für eine zweite Straße ist. Der Punkt ist, benannte Methoden machen Fehler deutlich:

image

Builder-Objekte sind offensichtlich sehr gut wiederverwendbar. Im folgenden Beispiel wird eine Basisstruktur, die an mehreren Stellen benötigt wird, einmal im Test-Setup konfiguriert und kann dann natürlich noch in Testfällen verfeinert werden. Bei der but() Methode, handelt es sich um Syntactic Sugar um die Tests – eh klar – besser lesbar und ausdrucksstärker zu gestalten.

image

Builder kann man natürlich auch wunderbar kombinieren und es kommt dann zu solch kompakten Konstruktionen wie im folgenden Beispiel. Die anOrder()-, aCustomer()- und anAddress()-Methode sind statische Hilfsmethoden die entsprechende Builder zurückgeben. Sie kapseln den “noisy” Code und verbessern dadurch Ausdrucksstärke und Lesbarkeit. 

image

Refactor to Builders:

Und so sehen dann entsprechende Tests aus, die reichlichen Gebrauch von Test Data Buildern machen. Die sendAndProcess()-Methode ist wieder ein Platz, wo Gemeinsamkeiten der beiden Orders hinzugefügt werden. Dabei werden Builder- und nicht konkrete Order-Objekte herumgereicht. Steven Freeman erwähnt auch, dass bis dato noch kein Code geändert wurde, sondern einfach darauf geachtet wurde, den Test-Code deklarativer zu gestalten.

image 

Die bisher gezeigten Beispiele waren eher prozedural angehaucht, also machen wir es deklarativer indem wir nach dem was und nicht nach dem wie fragen. Aus sendAndProcess wird havingReceived – eine einfache Änderung aber sehr ausdrucksstärk.

image

Dabei kann sich die Implementierung von havingReceived natürlich ändern, der Test bleibt jedoch in seiner Form bestehen. Jetzt beschreibt die Methode was der Autor gemeint hat anstatt wie er es wollte.

Test Diagnostics:

Angenommen man schreibt Tests für ein bestimmtes Projekt, jedoch wechselt man zwischendurch zu einem anderen Projekt. Nach einiger Zeit kehrt man zum ursprünglichen Projekt zurück. Das erste was man macht, man führt die TestSuites aus. Da eventuell einige Entwickler an dem Projekt weitergearbeitet haben, und gewisse Tests nicht angepasst haben, schlagen einige dieser Tests fehl. Jetzt hilft es natürlich ungemein, wenn man aussagekräftige Fehlermeldungen erhält, bei denen man die Ursache der Fehler sofort erkennen kann. Steven Freeman erklärt, dass die Fehlermeldung auf den ersten Blick erkennen lassen sollte, was die Ursache des Problems ist. Wenn das nicht der Fall sein sollte, bleibt einem oft nichts anderes übrig, als den Debugger  zu starten. Das hat zur Folge, dass unser Workflow unterbrochen wird und das will natürlich kein Entwickler der gerade "in the zone” ist. Ein erster Schritt um Tests möglich self-explanatory zu gestalten, ist das setzen einer Message in Assertions, wie das folgende Beispiel zeigt.

image

Im nächsten Beispiel wird das Überschreiben der toString()-Methode dafür verwendet, einem Datum einen aussagekräftigen Namen zu geben.  Dadurch erleidet der Entwickler überhaupt keinen Zeitverlust beim Herausfinden des Problems.

image

Freeman erwähnt auch noch, dass das Beschreiben von Werten gut für Entwickler-Tests, jedoch nicht unbedingt für Business-Leute geeignet sei (es passe nicht zu deren mentalen Modell). Doch prinzipiell ist es eine gute Idee, den Mocks Bezeichnungen zu geben, um aussagekräftige Fehlermeldungen zu bekommen.

image

Der übliche TDD-Workflow wird nun durch die Phase “Make the diagnostics clear” ergänzt, um die Tests so ausdrucksvoll und selbstbeschreibend wie möglich zu gestalten.

Text Flexibilität:

Brüchige Tests verlangsamen die Entwicklung. Deshalb müssen Tests zu jedem Zeitpunkt sauber bleiben (= Grüner Balken). Natürlich muss das Design des Produktionscodes ebenfalls dementsprechend gut strukturiert sein, sonst kann man zu einem späteren Zeitpunkt nicht mehr feststellen, welcher Teil, Tests oder Produktionscode, falsch ist.

Freeman erwähnt auch, dass die Information und nicht Repräsentation wichtig sei. Dabei bringt er wieder das Beispiel von selbstbeschreibenden Variablennamen.  Das retournieren von Null kann sehr oft die Intention des Autors nicht zum Ausdruck bringen, trotzdem wird es immer wieder als Rückgabewert verwendet.

Präzise Assertions und Erwartungen: Im zweiten Beispiel der folgenden Abbildung sieht man,  dass dem Autor nur wichtig war, dass die Transaktions-Id inkrementiert wird, jedoch nicht um wie viel. Hätte man einen Integrationstest wobei eine Datenbank involviert wäre, wäre es extrem aufwändig die Transaktions-Ids jedes mal zurückzusetzen bzw. die DB so zu konfigurieren, dass die Tests sauber bleiben. Das gilt natürlich nur für den Fall, dass man die genaue Schrittlänge der Inkrementierung im Test überprüfen wolle. Der Autor hält also seine Tests in diesem Fall flexibel und gut wartbar (Fokus liegt hier auf der “largerThan” Hilfsmethode).

image

Ebenso beim Beispiel bezüglich der failureMessage wird nicht der genaue String, jedoch bestimmte Werte die in der Message enthalten sein sollten, abgefragt. Das ist natürlich nur dann möglich, wenn der Testfall diese Flexibilität zulässt. Anders würde es aussehen, wenn man den genauen String, also die genaue Struktur der Message testen möchte. Das folgende Beispiel zeigt noch die entsprechenden Erwartungen mittels JMock passend zu den vorherigen Assertions.

image

Steven Freeman gibt noch den Tipp, Reihenfolgen in Testfällen zu erzwingen, wenn es wirklich notwendig ist. Dabei erwähnt er auch noch, dass Interfaces aussagen ob Objekte zusammenpassen, Protokolle jedoch feststellen, ob Objekte zusammenarbeiten können. Diese Aussagen beziehen sich auf das folgende Beispiel:

image

Dabei ermöglicht JMock das bestimmte Methodenaufrufe in einer bestimmten Reihenfolge aufgerufen werden müssen. Die searchFinished()-Methode muss dabei zuletzt aufgerufen werden.

Tests sind auch Code

Was man aus diesem Talk zumindest mitnehmen sollte ist, Ausdruckstärke sollte über Bequemlichkeit gestellt werden. Tests sollten so selbsterklärend wie möglich implementiert werden. Für Tests müssen die gleichen Prinzipien wie für Produktionscode gelten. Refaktorisierung und Abstrahierung sollten ebenso auf Tests angewendet werden, wie auf Produktionscode. Jeder Entwickler sollte in Tests das Fokussieren, was wirklich zählt.

Mein absoluter Lieblingspunkt ist: “Wenn es schwer zu testen ist, ist das ein Hinweis!

Wenn man sich unsicher ist, ob die eigene Code-Base gut testbar ist, sollte man sich einfach einmal irgendeine Klasse herauspicken und versuchen, diese zu testen.

Über sageniuz

https://about.me/ClausPolanka
Dieser Beitrag wurde unter Clean Code veröffentlicht. Setze ein Lesezeichen auf den Permalink.

Schreibe einen Kommentar

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s