Blog Mocking Frameworks verschlechtern das Software Design

Mocking Frameworks verschlechtern das Software Design

Die wenig aufwendige Herstellung von Mocks hemmt das Hinterfragen des Designs

Vorteile testgetriebener Entwicklung

n-design entwickelt Software testgetrieben. Test Driven Development (TDD) ist für uns nicht nur eine Methode zur Verringerung von Fehlern während der Entwicklung und beim fortlaufenden Refactoring. Das testgetriebene Vorgehen hat auch einen großen Einfluss auf das Design. Dies liegt u.a. an folgenden Aspekten:

  • Das Schreiben der Tests versetzt uns als Designer und Entwickler einer Schnittstelle in die Perspektive des Konsumenten der konstruierten API. Häufig fĂĽhrt dies zur Konsolidierung des Vertrags der Schnittstelle.
  • Durch die Tests entstehen mehrere Clients fĂĽr die Schnittstelle. Dies fĂĽhrt zu einer flexiblen und modularen Architektur.
  • Um Tests fĂĽr sinnvolle Einheiten schneiden zu können, mĂĽssen Teile, die nicht zum Testgegenstand gehören, austauschbar sein. Dies fördert eine lose Kopplung von Komponenten durch die Verwendung von Interfaces und Dependency Injection.

Der zuletzt genannte Aspekt ermöglicht erst das Schreiben von Unit-Tests. Sind Komponenten nicht austauschbar, kann die Software immer nur mit allen technischen Abhängigkeiten getestet werden. Dies ist nicht praktikabel, wenn diese Abhängigkeiten I/O, eine Datenbank oder das Netzwerk einbeziehen.

Was sind Unit-Tests?

Auf die Frage, was eigentlich die zu testende Einheit einer Software für einen Unit-Test ist, gibt es unterschiedliche Antworten. Fowler unterscheidet in seinem Beitrag „Mocks Aren't Stubs“ zwischen „Mockist and Classical Testing“. Beim Mockist-Style werden sämtliche Abhängigkeiten der Testeinheit durch Doubles ausgetauscht, während beim Classical-Style nur die schwierig zu kontrollierenden Abhängigkeiten ersetzt werden.

Interessant ist auch der Sinneswandel des Autors des Buches „The Art of Unit Testing“, Roy Osherove. War er zunächst der Ansicht, die Unit sei der kleinst-mögliche testbare Teil einer Software, also eine Methode einer Klasse, vertritt er heute die Meinung, eine Unit könne eine Methode, eine Klasse oder mehrere Klassen einbeziehen. Entscheidend ist, dass die Unit einen einzelnen Use Case abbildet (http://artofunittesting.com/definition-of-a-unit-test).

Aufwand der Testentwicklung und Bindung an Implementierung

Die Definition von Osherove ist nach meinen Erfahrungen praktikabel. Jede Abhängigkeit durch einen Mock zu ersetzen, schafft aus meiner Sicht nur einen geringen Mehrwert. Der besteht hauptsächlich darin, dass ein fehlgeschlagener Test vermeintlich unmittelbar auf den defekten Teil der Software verweist. Oder passt die Konfiguration eines Mocks nicht mehr zur vorgenommenen Code-Änderungen? Aus meiner Erfahrung ist die Recherche nach einem Defekt aber auch in größeren Test-Einheiten kein Problem und erfordert wenig Zeit.

Dem gegenüber steht der enorme Wartungsaufwand der Tests die im Mockist-Style entwickelt wurden. Ändern sich durch Doubles ersetzte Abhängigkeiten im Verhalten, dann ist häufig die Konfiguration aller Mocks diesbezüglich anzupassen. Verzichtet man hingegen auf Mocks an geeigneten Stellen, dann sind meist „nur“ die Assertions des Tests zu ändern.

Besonders aufwendig wird die Test-Wartung, wenn das Verhalten der Software mit „verify“ überprüft wird. Hierbei wird eine dem Entwickler bekannte Aufrufsequenz von Abhängigkeiten überprüft. So entsteht eine besonders intensive Bindung des Tests an die Implementierung. Der Test muss ebenfalls mit Änderung einer Aufrufsequenz angepasst werden.

Ich bevorzuge daher das Testen eine Schnittstelle und nicht deren Implementierung. Als Ausnahme hiervon wird häufig das Beispiel „Cache“ herangezogen. Dass z.B. nach Aufruf einer DAO-Methode der Cache entweder mit „put“ bzw. „get“ gerufen wurde, lässt sich in der Tat komfortable über ein „verify“ eines Mocks feststellen.

VerfĂĽhrerisch einfaches Mocken

Der hohe Wartungsaufwand, der durch das Mocken sämtlicher Abhängigkeiten entsteht, ist unabhängig davon, ob ein Mocking-Framework verwendet wird oder nicht. Die vereinfachte Konfiguration von Mock-Objekten durch entsprechende Frameworks kann aber einen negativen Einfluss auf den kontinuierlichen Design-Prozess der Software haben.

Ist das Mocken von Abhängigkeiten zu leicht, bietet das Schreiben der Tests keinen Anlass mehr, das Software Design ggf. zu überdenken. Nämlich genau dann, wenn man bei der Implementierung des Testfalls feststellt dass sich der gewünschte Test mit dem vorhandenen Design der Test-Einheit nicht herstellen lässt. Hier zwei Beispiele, bei denen dies besonders deutlich wird.

  • In einer zu testenden Klasse wird eine Abhängigkeit nicht per Dependency Injection verfĂĽgbar gemacht. Das fällt bei der Verwendung eines Mocking-Frameworks womöglich nicht auf, da hiermit ein privates Feld einer Klasse leicht fĂĽr den Test zur Laufzeit manipuliert werden kann. MĂĽsste man hierfĂĽr selber die Reflection-API bemĂĽhen, wĂĽrde einem vermutlich auffallen, dass hier ein Design-Fehler vorliegt und die Abhängigkeit besser in die Test-Klasse injiziert wird.
  • Eine Abhängigkeit im Test-Kandidaten liegt als Aufruf einer statischen Methode einer weiteren Klasse vor. Auch das Verhalten der statischen Methode lässt sich z.B. mit PowerMock manipulieren. Wäre dieses Tool nicht verfĂĽgbar, könnte auffallen, dass die harte Bindung durch Verwendung statischer Abhängigkeiten besser durch einen objektorientierten Ansatz ersetzt wird.

Da die Herstellung eines Doubles mit Mocking-Frameworks sehr wenig Aufwand erzeugt, führt es auch nicht dazu, dass Abhängigkeiten generell minimiert werden. So fördern sie nicht das Schneiden kleiner Komponenten im Sinne des Single Responsibility Principles und können so eine schwer zu wartende Komplexität schaffen.

NĂĽtzliche Helfer

Die Verwendung von Mocking-Frameworks vereinfacht die Herstellung eines Mocks enorm. Es ist deutlich angenehmer, z.B. genau die eine benötigte Methode eines Data Access Objects mit einem Mocking-Framework für den Test zu konfigurieren als dafür ein gesamtes Interface implementieren zu müssen. Daher verwende auch ich gerne Mocking-Frameworks. Allerdings ist darauf zu achten, dass die Leichtigkeit der Anwendung nicht das kontinuierliche Hinterfragen des eigenen Software Designs hemmt.

Muss in einen Test Legacy Code einbezogen werden, können auch Tools wie z.B. PowerMock, die u.a. das Mocken statischer Methoden erlauben, nützlich sein. Der Einsatz sollte sich aber auf die nicht änderbaren Code-Bestandteile beschränken.

Für mich haben sich folgende Leitlinien bei der Entwicklung von Tests bewährt:

  • Die zu testende Unit ergbit sich aus dem zu testenden Use Case. Eine Definition entlang von Code-Strukturen (Methode, Klasse, Modul etc.) ist nicht praktikabel.
  • Abhängigkeiten werden durch Mocks ersetzt, wenn diese schwer zu kontrollieren sind. Ansonsten können Abhängigkeit mit „echten“ Implementierungen verwendet werden, auch wenn dadurch „Mini-Integrationstests“ entstehen.
  • Mocks werden je nach Aufwand mit oder ohne Mockking-Framework hergestellt.
  • Mocking-Frameworks werden vorwiegend zur Konfiguration des Verhaltens eines Mocks verwendet. Weitergehende Funktionalitäten, wie z.B. „verify“, sind mit Bedacht zu verwenden. Hier gibt z.B. die Dokumentation von Mockito hilfreiche Hinweise, indem an einigen Stellen auf die „Risiken“ der Verwendung hingewiesen wird; z.B hier bezĂĽglich Spy http://docs.mockito.googlecode.com/hg/org/mockito/Mockito.html#13 und hier zum Verhältnis von „stub“ und „verify“ http://docs.mockito.googlecode.com/hg/org/mockito/Mockito.html#2.