90 Prozent Testabdeckung durch gutes OO-Design (Teil 1)

11.04.2018

In meinem kürzlich veröffentlichten Blogbeitrag Code Coverage Reports mit Maven in Multi-Modul-Projekten habe ich beschrieben wie man Coverage-Reports mit Maven erstellt.

Coverage Reports – wozu eigentlich?

Doch wozu benötigt man Coverage Reports? Wie der Name sagt, misst man
damit die Testabdeckung, also den Grad, in dem Produktivcode durch automatisierte Tests abgedeckt ist. In der Regel nutzt man Code Coverage Reports, wenn man ein definiertes Mindestmaß an Testabdeckung erreichen möchte. Schließlich erleichtert gut getesteter Code den Entwicklern das Leben, weil viele durch Änderungen entstandene Bugs frühzeitig durch die Tests aufgedeckt werden. Das spart Geld, da Bugfixing in der Regel immer aufwändiger und teurer wird, je später ein Fehler entdeckt wird. Zudem nehmen die Tests den Entwicklern die Scheu vor Refactoring, was essenziell für eine dauerhaft wartbare, verständliche und änderbare Codebasis ist.

Doch wie erreicht man überhaupt eine hohe Testabdeckung? Und was hat das Ganze mit gutem OO Design zu tun? In diesem Artikel möchte ich versuchen, ein paar Antworten auf diese Fragen zu geben.

Guter Code ist testbarer Code
Eine hohe Testabdeckung setzt voraus dass es leicht möglich ist, Unit Tests für den Code zu schreiben. Erfreulicherweise geht gutes objektorientiertes Design mit einer guten Testbarkeit einher1. Löst man Probleme im Design, werden in der Regel gleichzeitig auch Probleme beim Testen gelöst. Allerdings gilt dies nicht umgekehrt. Das Lösen von Testproblemen führt nicht automatisch zu besserem Design!2

Doch wie zeichnet sich gutes objektorientiertes Design aus? Und wie erkennt man mangelhaftes Design?

„Design Smells“: Wann das Design verbessert werden sollte

Beginnen wir mit letzterem und schauen uns sogenannte „Design Smells“ an – also Anzeichen dafür, dass das Design verbessert werden sollte. Eine Auswahl an Beispielen zeigt folgende Tabelle (ohne Anspruch auf Vollständigkeit):

1. Enge Kopplung zwischen Klassen oder Komponenten
2. Eine Änderung an einer Stelle bewirkt Fehler an vielen anderen Stellen, z.B. bedingt durch globalen oder geteilten Zustand.
Beispiele: Änderbare Instanzen (durch Singleton-Pattern erzwungen), öffentliche statisch Felder oder geteilte veränderliche3 Datenobjekte oder Collections
3. Konstruktoren, die neue Objekte erzeugen oder Arbeit verrichten, die nicht ausschließlich der Initialisierung des zu erzeugenden Objekts dient
4. Viele Konstruktor- bzw. Methodenparameter
5. Verletzung des Geheimnisprinzips bzw. des „Law of Demeter
6. Das Bedürfnis, PowerMock einzusetzen, um beispielsweise statische Methoden oder Konstruktoren zu mocken
7. Der Wunsch, private Methoden testen zu wollen
8. Die Verwendung von @InjectMocks in Tests4
etc.

Kriterien für gutes OO Design

Ein gutes objektorientiertes Design zeichnet sich u.a. aus durch die in der folgenden Tabelle gelisteten Aspekte:

1. Einhaltung der SOLID-Prinzipien. Da eine Erklärung der einzelnen Prinzipien an dieser Stelle zu weit führen würde, wird auf die entsprechenden Quellen verwiesen:

2. Lose Kopplung zwischen Klassen und Modulen
3. Prinzip der Verschwiegenheit (Information Hiding)
4. Viele kleine, dafür spezialisierte Interfaces, Klassen und Methoden (eine Folge des Single Responsibility Principles)
5. Aufgaben von Typen und Methoden spiegeln sich in deren Namen wider
6. Keine Überraschungen („Principle of Least Astonishment“): Der Code macht das, was die Namensgebung von Klassen, Methoden etc. verspricht
7. Wenn möglich Verwenden von unveränderlichen (immutable) Datentypen. Werden veränderliche Datentypen oder Collections nach außen gegeben oder via Settermethode gesetzt, sollten Kopien angefertigt werden. Damit werden unerwartete Seiteneffekte durch indirekte Zustandsänderungen vermieden
8. Query Command Separation: Eine Methode setzt entweder einen Wert oder liefert einen Wert zurück, macht aber niemals beides gleichzeitig
etc.
Testbaren Code schreiben

Jeder Entwickler der schon einmal Tests für Bestandscode schreiben musste, der nicht testgetrieben entwickelt wurde, hat sicher diese Erfahrung gemacht: Es ist oft schwer, den zu testenden Code nachträglich in ein Testgerüst zu „zwängen“. Beispielsweise wenn die zu testende Klasse intern selbst abhängige Objekte erzeugt, auf die man von außen keinen Einfluss hat. Oder wenn sie globale, via Singleton-Pattern erzeugte, Instanzen nutzt. Auch beim Einsatz von Mocking-Frameworks wie Mockito kann das Testsetup schwierig sein. Das ist unter anderem der Fall, wenn das Testsubjekt sich mit verketteten Aufrufen von Getter-Methoden in die Interna eines abhängigen Objekts „gräbt“, um eine Operation auf einem tief verschachtelten Objekt durchzuführen (z.B. a.getB().getC().getD().setActive(true);5 ). In so einem Fall muss man diese Verschachtelung aufwändig mit einer passenden Hierarchie von Mocks nachstellen.

Wie aber gestaltet man seinen Code so, dass er gut testbar ist?

Tests should be F.I.R.S.T.

Eine gute Methode ist – man glaubt es kaum – die testgetriebene Entwicklung. Werden die Tests zuerst geschrieben, entsteht automatisch testbarer Produktivcode. Zudem bietet Test Driven Development den Vorteil, dass man sich gleich zu Beginn überlegt, wie der Code aus Client-Sicht aussehen soll, und welche etwaigen Grenzfälle und Ausnahmen existieren, die durch Testfälle abgedeckt werden können.

Eine hilfreiche Gedächtnisstütze für die Beschaffenheit von guten Tests ist „Tests should be F.I.R.S.T.“. Dabei ist mit F.I.R.S.T. nicht (nur) gemeint, dass die Tests vor dem eigentlichen Produktivcode geschrieben werden sollen. Dahinter verbergen sich die Begriffe Fast, Isolated/Independent, Repeatable, Self-validating sowie Thorough&Timely 6.

Dependency Injection – das ultimative Design Pattern

Dreh- und Angelpunkt von testbarem Code ist Dependency Injection (DI). Auch wenn es sich dabei nicht um eins der klassischen Patterns aus dem Standardwerk der „Gang of Four“7 handelt, ist es ein Design Pattern, das sich – nicht nur – im Bereich von Enterprise-Anwendungen etabliert hat. In Spring sowie JEE/CDI ist Dependency Injection eine der Grundfunktionalitäten. Darüber hinaus hat Google mit Guice ebenfalls ein leichtgewichtiges DI-Framework entwickelt.

Dependency Injection folgt dem Prinzip der „Inversion of Control“ (IoC). Das bedeutet, dass Objekte sich ihre Kollaborateure8 nicht selbst beschaffen oder erzeugen, sondern sie stattdessen von außen „injiziert“ bekommen.

Für das Testen ist dies insofern vorteilhaft, dass Testdoubles bzw. Mocks anstelle der eigentlichen Kollaborationsobjekte injiziert werden können. Mit Hilfe der Mocks erfolgt das Testsetup, indem man sie für den jeweiligen Testfall definierte Testdaten zurückgeben lässt. Anschließend kann man testen, ob sich das Testobjekt wie erwartet verhält, indem man prüft ob z.B. eine Methode mit den erwarteten Parameter aufgerufen oder eine Exception wie erwartet geworfen wurde.

Dependency Injection ermöglicht das isolierte Testen einer Klasse unabhängig von ihren Kollaborateuren und erlaubt ein recht einfaches Testsetup – insbesondere wenn man es durch Einhaltung des Single Responsibility Prinzips mit kleinen Klassen und einer geringen Anzahl von Abhängigkeiten pro Klasse zu tun hat.

Gleichzeitig führt Dependency Injection zu besserem Code-Design, weil dadurch wiederum das Single Responsibility Prinzip gefördert wird: Schließlich wäre das Beschaffen oder Erzeugen seiner Abhängigkeiten ein weiterer Verantwortungsbereich, den ein Objekt neben seiner Hauptaufgabe übernimmt. Bei Dependency Injection wird die Erzeugung der Objekte inklusive all ihrer Abhängigkeiten von einer eigens dafür bestimmte Instanz übernommen, nämlich den IoC-Container des DI-Frameworks9. Ein weiter Design-Aspekt ist, dass man dem Objekt statt einer konkreten Implementierung ein Interface als Abhängigkeit injizieren kann, das nur die vom aufrufenden Objekt benötigten Methoden enthält. Dadurch verringert sich die Kopplung zwischen den beiden Objekten, und es ist möglich, die Implementierung später auszutauschen, ohne den Code des aufrufenden Objekts ändern zu müssen. Zudem kann man durch die Benennung des Interface explizit die Rolle ausdrücken, die das Kollaborationsobjekt für das Client-Objekt spielt10 , was die Verständlichkeit des Codes erhöht.

Constructor Injection, bitte sehr

Ein Designprinzip besagt, dass es nur möglich sein sollte gültige Objekte zu erzeugen. Im Zusammenhang mit Abhängigkeiten heißt das, dass ein Objekt von Anfang an alle seine Kollaborationsobjekte haben sollte, die es zur Erledigung seiner Arbeit benötigt. Dies kann nur durch Constructor Injection gewährleistet werden, d.h. indem die Abhängigkeiten dem Objekt als Constructor-Parameter mitgegeben werden. Bei Setter- oder Field Injection, also dem Setzen der Abhängigkeiten via Setter-Methode bzw. Reflection, läuft man Gefahr, dass Abhängigkeiten uninitialisiert bleiben und als Folge davon NullPointerExceptions fliegen könnten.

Darum sollte man wann immer möglich ausschließlich Constructor Injection verwenden. Das Erzeugen von ungültigen, nicht arbeitsfähigen Objekten ist dann nicht möglich.

Ein weiterer Vorteil zeigt sich darin, dass eine zu große Anzahl an Abhängigkeiten direkt am Konstruktor sichtbar wird. Hat dieser zu viele Parameter, weist die Klasse eine hohe Kopplung auf und verletzt höchstwahrscheinlich das Single Responsibility Prinzip. Dies ist ein „Design Smell“ der darauf hindeutet dass es Zeit ist, einen Teil der Funktionalität in eine neue Klasse auszulagern.

Was tun bei „Production Code First“

Leider befindet man sich nicht allzu oft in der komfortablen Situation, neuen Code „auf der grünen Wiese“ schreiben zu dürfen. Meist arbeitet man an Bestandssystemen, und oft an solchen die (noch11) nicht testgetrieben entwickelt wurden. Wie vorher schon erwähnt, ist es in der Regel schwierig, nachträglich Tests für solchen Code zu schreiben. Hier sollte dem Bestandscode durch Refactoring nach und nach zu einem besseren Design zu verholfen werden. Oft müssen dazu Abhängigkeiten aufgebrochen werden damit es möglich wird, Testcode zu schreiben. Dieser ist wichtig um sicher zu stellen, dass beim Refactoring nichts in die Brüche geht.

Eine ganze Reihe solcher Techniken zum Aufbrechen von Abhängigkeiten („dependency breaking techniques“) sowie generell für die Arbeit mit Bestandscode beschreibt Michael Feathers in seinem Buch „Working Effectively with Legacy Code“.

Fazit

Gutes OO Design geht mit Testbarkeit des Codes einher. Testgetriebene Entwicklung ist die beste Methode zum Schreiben von testbarem Code. Dependency Injection – genauer gesagt Constructor Injection – im Zusammenspiel mit dem Single Responsibility Principle führt zu kleinen spezialisierten Klassen und Interfaces, die lose gekoppelt sind und isoliert voneinander getestet werden können. Hat man es mit Legacy-Code zu tun, sollte das Design während der Weiterentwicklung Schritt für Schritt verbessert werden. Hier helfen Techniken, mit denen Abhängigkeiten aufgebrochen werden können.

Im zweiten Teil möchte ich an Praxisbeispielen zeigen, wie man Code so designen kann dass er gut testbar ist.

Mehr zu Testing erfahren

 


Quellen:

1 The Deep Synergy Between Testability and Good Design: https://vimeo.com/15007792 ^

2 Das Ändern einer private-Methode nach public nur um sie testen zu können führt in der Regel nicht zu besserem Design, eher das Gegenteil ist der Fall ^

3 „mutable“ ^

4 Eine Ausnahme stellt die Field-Injection von EntityManager-Instanzen dar, da es im Enterprise-Umfeld keine Möglichkeit gibt, sich diese via Constsructor Injection injizieren zu lassen ^

5 Solche Konstrukte werden auch als „Train Wreck“ bezeichnet. Sie stellen eine Verletzung des Law of Demeter dar. Der Zugriff auf die interne Struktur des Objekts führt zu einer engen Kopplung. ^

6 siehe auch F.I.R.S.T Principles of Unit Testing ^

7 Design Patterns. Elements of Reusable Objekt-Oriented Software, von Erich Gamma, Richard Helm, Ralph E. Johnson, John Vlissides ^

8 Objekte, mit denen ein anderes Objekt zusammen arbeitet, um seine Aufgabe zu erledigen, wie etwa Services, Repositories, u.ä. – auch „abhängige Objekte“ oder „Abhängigkeiten“ ^

9 Dependency Injection geht übrigens auch ohne ein DI-Framework. In kleinen Anwendungen kann die Objektstruktur auch einfach innerhalb der Main-Methode erzeugt werden. ^

10 Siehe „Growing Object Oriented Software, Guided by Tests“ von Steve Freeman und Nat Pryce ^

11 Ungeachtet der Tatsache, dass Test Driven Development mittlerweile immer größere Akzeptanz findet, wenn nicht gar bereits zum „guten Ton“ gehört, gibt es noch viele solche Systeme ^

Zurück zur Übersicht

2 Kommentare zu “90 Prozent Testabdeckung durch gutes OO-Design (Teil 1)

  1. Hallo Stefan,

    du hast absolut Recht mit den Punkten, die du aufführst! Vielen Dank für die ausführliche Erläuterung.

    Freue mich schon auf den zweiten Teil!

    Grüße
    Stefan

Kommentar verfassen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

*Pflichtfelder

*