Testbarer Code – Konstruktoren Teil 1

Im Zeitalter von Test Driven Development, von Dependency Injection und der Clean Code Bewegung, sehe ich doch noch äußerst häufig Code, der einfach überhaupt nicht bzw. nur sehr schwer testbar ist. Meistens wird dieser Code aus diesem Grund auch erst gar nicht getestet. Das verschlechtert natürlich das Test Coverage. Voraussetzung für eine gute Testabdeckung ist natürlich, dass überhaupt getestet wird. Manche Entwickler, ich eingeschlossen, fragen sich, wie es heutzutage überhaupt möglich ist, Software zu entwickeln, ohne das Hauptartefakt, nämlich den Quellcode zu testen. Ist es wirklich besser, knifflige Fehler in langwierigen Debugging-Sessions zu finden? Machen Änderung an einer umfangreichen Code-Base wirklich Spaß, ohne die Sicherheit zu haben, dass alle Tests positiv ausgeführt werden? Wäre es nicht besser, eine bzw. mehrere gut funktionierende Test-Suiten zu implementieren und somit Sicherheit bezüglich des Quellcodes entwickeln zu können.

Angenommen ich konnte mit dem vorherigen Absatz ein bisschen überzeugen, dass Tests etwas Gutes sind. Dann stellt sich die Frage wie man seinen Quellcode dem man täglich schreibt, gut testbar macht. Stellt sich wirklich die Frage? Jeder kennt XUnit und jeder kann Assert-Anweisungen schreiben, aber warum sehe ich dann noch so oft Code wie in der folgenden Form:

image

Abgesehen von den fachlichen Umständen😉 wenn man bei dieser Art von Design nicht erkennt, dass es sich um schwer testbaren Code handelt, dann sollte man unbedingt weiterlesen oder aber einfach einmal versuchen eine solche Klasse zu testen. Angenommen man möchte die Funktionalität von der House-Klasse testen, also schreibe ich konkret den folgenden Test:

image

Mist! Wie kann ich die beiden Assoziationsklassen, Kitchen und Bedroom, konfigurieren bzw. durch Mock- oder Dummy-Objekte ersetzen? Die Beziehungen zu diesen Klassen sind in der House-Klasse festgeschrieben ( = starke Kopplung). Es ist unmöglich die House-Klasse ohne diese beiden Klassen zu testen. Wenn man jetzt immer noch nicht überzeugt davon ist, dass House nicht gut testbar ist, dann sollte man sich einmal überlegen, was wenn die Erzeugung von Kitchen oder Bedroom den Zugriff auf externe Ressourcen (Datenbank, etc.) erfordern? Sollten Unit-Tests nicht isoliert und performant laufen? Natürlich! Deshalb sollte man in Unit-Tests losgelöst von etwaigen Zugriffen auf externe Ressourcen ausführen und unbedingt die beiden Assoziationsklassen durch Mock- oder Dummy-Objekte ersetzen. Aber wie? Einfach weiterlesen😉

Konstruktoren sollten keine Logik beinhalten 

Im vorherigen Code-Beispiel wurde nur eines von mehreren Problemen, die bei einem solchen Design zum Vorschein kommen, beschrieben. Möchte man seinen Code testbar gestallten, sollte man folgende Dinge beachten:

In Konstruktoren sollten keine Assoziationsobjekte erzeugt bzw. instanziiert, keine anderen Services aufgerufen und keine Logik ausgeführt werden, um den Objekt-eigenen Zustand zu setzen. Sobald man eines dieser Dinge im Konstruktor festschreibt, entfernt man automatisch die für das Testen so wichtige Beziehungsgrenze. Außerdem mutet man eventuellen Subklassen oder Mock-Objekten ungewolltes Verhalten zu.

Eine Sache ist ganz klar, erledigt man mehr als nur Zuweisungen in Konstruktoren, wird es gar unmöglich, Objekte zu denen eine Beziehung besteht, zu konfigurieren bzw. durch Mock-Objekte zu ersetzen.

Anhand des vorherigen Beispiels sieht man, dass so ein Design äußerst unflexibel und eine starke Kopplung zwischen den Objekten mit sich bringt. Das Ersetzen der beiden Assoziationsobjekte durch Mock-Objekte ist in diesem Beispiel nicht möglich und erschwert das Testen ungemein.

Das folgende Beispiel zeigt die Initialisierung des Kitchen-Objekts durch einen Service-Call des DatabaseService-Objekts. Solche Konstruktoren zu testen sind äußerst problematisch. Beim Erzeugen eines House-Objekts wird dieser Konstruktor auf jeden Fall ausgeführt und somit erfolgt der Datenbankzugriff bei jedem Erstellen eines neuen Hauses. Jeglicher Zugriff auf externe Ressourcen im Konstruktor führt zu Performance-Problemen und ein Ersetzen ist schlichtweg unmöglich.

image 

Unit-Tests sollten isoliert, also losgelöst von externen Ressourcen, entwickelt werden, damit sie oft und performant ausgeführt werden können.

Des weiteren verletzt man das Single-Responsibility-Principle sobald man Assoziationsklassen im Konstruktor erzeugt und den eigenen Zustand des Objekts initialisiert. Außerdem wird dadurch die Wiederverwendung bzw. die Konfigurierbarkeit des House-Objekts erheblich eingeschränkt, da man ja keine alternative Implementierung des Kitchen-Objekts dem House-Objekt übergeben kann.

Bei der Erzeugung eines Objektgraphen handelt es sich um eine eigenständige Verantwortlichkeit die von der Instanziierung eines Objekts getrennt werden sollte.

Zweifelhafter Workaround

Ein möglicher Workaround um eine Klasse für Tests zu konfigurieren zeigt folgendes Beispiel:

image

Im Konstruktor der House-Klasse wird eine Methode aufgerufen, um die Bedroom-Klasse zu instanziieren. Diese Methode kann in Tests durch erzeugen einer Subklasse von House überschrieben werden. Dadurch kann z.B.: eine “lightweight” House-Instanz erzeugt werden. Wenn man jedoch davon ausgeht, dass in dieser Methode in der House-Klasse eine Menge an Arbeit verrichtet wird, und diese Methode eigentlich getestet gehört, kann diese Alternative nur der letzte Ausweg sein. In der folgenden Abbildungen kann man noch die Subklasse betrachten:

image

Und hier der zugehörige Test:

image

Test-Konstruktoren

bieten natürlich auch keinen Ausweg aus der Misere. Solange es Klassen gibt, die den schwer zu testenden Konstruktor verwenden, kann man zwar die Klasse mit den Test-Konstruktoren isoliert testen, aber die nutzenden Klassen allerdings nicht. Z.B. besitzt die folgende House-Klasse einen “Test-Only” Konstruktor der in Unit-Tests verwendet wird. Allerdings verwendet die HouseClient-Klasse den schwer zu testenden Konstruktor wodurch die HouseClient-Klasse schwer zu testen wird.

image image image

Zusammengefasst kann man sagen, dass es einfach darauf ankommt, wie schwer es ist, eine Klasse in Isolation zu testen und wie schwer Klassen, zu denen eine Beziehung bestehet, durch Dummies ersetzet werden können. Wenn es extrem schwer ist, dann leistet der Konstruktor eventuell zu viel Arbeit. Ist es jedoch leicht, dann darf man sich selbst auf die Schulter klopfen.

Man wäre daher gut  beraten, wenn man beim Entwurf einer Klasse darüber nachdenkt, wie schwer diese Klasse zu testen wäre. Ist es leicht diese Klasse mit Hilfe des entworfenen Konstruktors zu erstellen? Man sollte auch immer berücksichtigen, dass die entworfene Klasse nicht nur im Unit-Test aufgerufen wird.

So viele Entwürfe sind voll von “objects that instantiate other objects or retrieve objects from globally accessible locations. These programming practices, when left unchecked, lead to highly coupled designs that are difficult to test.” [J.B. Rainsberger, JUnit Recipes, Recipe 2.11]

Ausschau halten nach folgenden Symptomen in Konstruktoren:

  • Erzeugt das Schlüsselwort new irgendetwas was man gerne durch ein Testdoubel ersetzen möchte? Dazu zählen alle Dinge die größer als irgendwelche Wertobjekte sind.
  • Gibt es irgendwelche Aufrufe von statischen Methoden? (Zur Erinnerung, statische Methoden können nicht durch Mocks ersetzt werden. Sieht mal also irgendetwas ähnliches wie z.B. Server.init(), sollten alle Warnsignale in jedem Entwicklerkopf aktiviert werden.
  • Gibt es irgendwelche Verzweigungen oder Schleifen? Man bedenke, dieser Code wird beim Erzeugen jeder Instanz ausgeführt.

Fundamentale Frage

Wie werde ich diese Klasse testen?

“If the answer is not obvious, or it looks like the test would be ugly or hard to write, then take that as a warning signal. Your design probably needs to be modified; change things around until the code is easy to test, and your design will end up being far better for the effort.” [Hunt, Thomas. Pragmatic Unit Testing in Java with JUnit]

Hinweis

Das Erzeugen von Wertobjekte ist absolut legitim. Z.B. LinkedList; HashMap; EmailAddress, CreditCard, … Die Haupteigenschaften solcher Klassen sind: Sie sind einfach zu erzeugen, sie sind Zustands-fokussiert und besitzen keine bzw. nur wenig Verhalten und sie referenzieren keine Service-Objekte.

Im zweiten Teil werde ich darüber berichten, wie man Klassen entwerfen kann, die einfach zu testen sind. Wie sich herausstellen wird, ist die Lösung dieses Problems nicht alt zu schwer. Ich werden anhand von einigen Code-Beispielen zeigen, wie man mit Hilfe von Dependency Injection Frameworks oder aber auch mittels Do-It-Yourself-DI testbaren Code entwerfen kann.

Über sageniuz

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

Eine Antwort zu Testbarer Code – Konstruktoren Teil 1

  1. Pingback: Anonymous

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