Spring Boot Unit-Tests mit Mockito – 5 einfache Lösungen für die häufigsten Probleme

Eine besondere Stärke von Spring Boot ist die nahtlose Integration diverser nützlicher Bibliotheken und Tools. Mockito ist beispielsweise eine hervorragend angebundene und sinnvolle Ergänzung. In diesem Artikel zeige ich, wie Du Mockito in eine bestehende Spring Boot Applikation einbindest und welche Möglichkeiten es bei der Verwendung gibt.

Die meisten unserer Applikationen stehen nicht alleine auf weiter Flur. Auch wenn es wirklich herausfordernd ist, eine „richtige“ Microservice-Architektur umzusetzen, interagieren doch so gut wie alle Applikationen mit ihrer Umwelt. Wer das Prinzip des Test-Driven-Developments (TDD) ernst nimmt, steht früher oder später vor einem Dilemma: Während der Ausführung der Unit-Tests im Projekt stehen die Abhängigkeiten ggf. nicht zur Verfügung oder sollen nicht aufgerufen werden. Denken wir zum Beispiel an einen Server zum Mailversand: Dieser soll natürlich nicht jedes Mal aufgerufen werden um eine Registrierungsmail zu verschicken, wenn wir einen User für unseren Test anlegen… Das ist jetzt kein konstruiertes Beispiel, die Folgen können verheerend sein, glaubt mir ;-)

Was also tun? Wie ich in den Posts über H2 und Testcontainers bereits geschrieben habe, versuche ich meine Projekte so aufzusetzen, dass zumindest die Unit-Test direkt nach dem Klonen des Codes sofort ausführbar sind. Insbesondere sollte niemand daran denken müssen, den Mailversand von Hand zu unterbinden, bevor er das erste Mal die Tests startet… Ihr könnt das in euren Teams gerne mal probieren, aber verspreche euch, irgendjemand vergisst das garantiert mal und dann gibt es unter Umständen richtig viel aufzuräumen ;-)

Glücklicherweise gibt es für diese Themen bereits Lösungen. Eine davon stelle ich euch heute vor. Die Bibliothek Mockito erlaubt uns im Zusammenspiel mit Spring Boot mit sehr eleganten Mitteln für Unabhängigkeit in unseren Applikationen zu sorgen.

Wie funktioniert Mockito?

Einfach gesagt kann Mockito in die „Kleider“ einer beliebigen Java Klasse schlüpfen. Das sind natürlich keine Jogginghosen oder Jacken, sondern in unserem Fall öffentliche Methoden und Eigenschaften der Klasse. Unser Code verwendet dann nicht mehr die eigentliche Java Klasse, sondern ein sogenanntes Mock. Zur Verdeutlichung der grundsätzlichen Funktionsweise, hier ein Snippet von der offiziellen Seite des Projekts Mockito:

// you can mock concrete classes, not only interfaces
LinkedList mockedList = mock(LinkedList.class);

// stubbing appears before the actual execution
when(mockedList.get(0)).thenReturn("first");

// the following prints "first"
System.out.println(mockedList.get(0));

// the following prints "null" because get(999) was not stubbed
System.out.println(mockedList.get(999));

Quelle: https://site.mockito.org/

In der ersten Codezeile wird das Mock mit der statischen Methode mock erzeugt. Die Variable mockedList ist jedoch weiterhin vom Typ LinkedList, es können also alle öffentlichen Methoden darauf aufgerufen werden! In der nächsten Zeile wird ein bestimmter Aufruf überschrieben, im Mockito-Jargon nennt sich das „Stubbing“. Wenn die Methode get mit dem Parameter 0 auf dem Mock aufgerufen wird, soll das Ergebnis „first“ ausgegeben werden.

Soviel zur Grundidee hinter Mocking und Stubbing, in Spring Boot wollen wir uns natürlich nicht selbst um die Erstellung der Objektinstanzen kümmern. Aber dafür gibt es entsprechende Annotationen zur nahtlosen Integration. Aber vorher sollten wir uns natürlich fragen, warum eine Verwendung von Mockito in Spring Boot überhaupt Sinn macht.

Warum Mockito in Spring Boot verwenden

Abgesehen von alternativen Möglichkeiten, Applikationen und insbesondere die Unit-Tests unabhängig von externen Abhängigkeiten zu gestalten, bietet sich Mockito hervorragend für die Verwendung in Spring Boot an. Das Grundprinzip Inversion of Control, auf dem das Spring Framework aufbaut, passt perfekt zum Konzept von Mockito.

Die Integration in Spring Boot ermöglicht es, die Mocks wie normale Beans zu behandeln, also auch an anderer Stelle zu verwenden, ohne den Produktionscode anpassen zu müssen. Denkt kurz zurück an das Beispiel mit dem Mail-Server: Die Idee wäre hier, für alle oder bestimmte Tests einen Mock zu verwenden, der den tatsächlichen Mailversand eben nicht aufruft. Mit diesem Konzept lässt sich unsere Applikation gut mit Tests versehen, die unabhängig von der Umgebung laufen können. Dabei bleibt unser Produktionscode aber aufgeräumt.

Einbindung von Mockito in Spring Boot

Typisch Spring Boot: Eigentlich gibt es so gut wie gar nichts zu tun ;-) Die eine Dependency – die wir hoffentlich alle per Default in unsere Projekte einbinden – genügt:

<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-test</artifactId>
     <scope>test</scope>
</dependency>

Damit bindet Spring Boot uns ein Framework für die Unit-Tests ein (aktuell ist der Default JUnit 5) und netterweise kommt Mockito auch gleich mit. Mit seinem Standardumfang – und vielen weiteren „Adapter-Komponenten“ in Richtung Spring Boot. Aber dazu gleich mehr ;-)

@Mock in Spring Boot Unit-Tests einbinden

Beim Beispiel von der Seite von Mockito bin ich vorhin über die erste Codezeile gestolpert:

// you can mock concrete classes, not only interfaces
LinkedList mockedList = mock(LinkedList.class);

Quelle: https://site.mockito.org/

Wie oben schon geschrieben, wollen wir uns in Spring Boot nach Möglichkeit nicht um die Instanziierung kümmern. Etwas schöner lässt sich das dann als Klassenvariable schreiben:

@Mock
private LinkedList mockedList

Das obige Beispiel könnten wir dann geringfügig anpassen, um es gegen die Klassenvariable laufen zu lassen. Würde so weit funktionieren, aber wirklich viel können wir damit noch nicht machen…

Der Mock würde in dem Fall nur in der definierten Klasse zur Verfügung stehen. Für manche Anwendungsfälle mag das eine gute Idee sein, aber im Allgemeinen arbeiten wir in Spring Boot ja mit unseren Beans, die vom Spring Context verwaltet werden. Das hat insbesondere zur Folge, dass die Beans dann auch an anderer Stelle injiziert werden. Auch dafür gibt es natürlich eine Lösung!

Verwendung von @MockBean in Spring Boot Unit-Tests

Zurück zum Beispiel mit dem Mailversand. Angenommen, wir haben einen einfachen Spring Service im Projekt, der Mails verschicken kann.

@Service
public class MailService {
     public long sendMail(String recipient, String subject, String text) {
          /* … implementation … */
     }
}

Von hier aus wird der Mailserver aufgerufen, die Mail also tatsächlich in den Versand gegeben. Würden wir jetzt in unserer Testklasse ein Mock für den MailService erzeugen, wäre das schön – würde aber ungefähr gar nichts bringen… Denn das Mock wird wirklich nur in dieser Klasse verwendet, aber eben nicht bei der Dependency Injection berücksichtigt. Aber genau das wollen wir ja eigentlich: Wir wollen überall dort im Projekt ein Mock verwenden, wo bisher der MailService injected wurde.

Natürlich ist das auch möglich ;-) Spring Boot hat dafür die Annotation @MockBean eingeführt. Damit wird einerseits ein Mock initialisiert, das andererseits als Spring Bean im Kontext registriert wird. Das beste aus beiden Welten also ;-)

@MockBean
private MailService mailServiceMock;

Jetzt haben wir die komplette Kontrolle: Alle anderen Komponenten, die den MailService verwenden bekommen jetzt unser Mock untergeschoben. Wir können die Methode zum Mailversand jetzt einfach stubben:

when(mailServiceMock.sendMail(anyString(), anyString(), anyString())).thenReturn(5493L);

Im Gegensatz zum Stubbing aus dem Mockito Beispiel, habe ich die Parameter sozusagen mit Wildcards versehen: mit diesem Stubbing werden wirklich alle Aufrufe mit beliebigen Parametern aufgesammelt und durch den Stub behandelt. Zurückgegeben wird dann immer der konstante Wert 5493L.

Wir könnten sogar noch ein feineres Stubbing definieren, um bestimmte Fälle gesondert zu behandeln:

when(mailServiceMock.sendMail(eq(„info@mischok.de“), anyString(), anyString())).thenReturn(9354L);

Dieses Stub würde jetzt alle Aufrufe mit der Empfängeradresse „info@mischok.de“ fangen und behandeln.

Aber was passiert denn mit den Aufrufen, die nicht von einem Stub abgedeckt werden? Einfach Antwort: Der Default. Beim Methodenaufruf passiert gar nichts, zurückgegeben wird der Default des Rückgabedatentyps. In unserem Beispiel mit dem Typ long wäre das 0, bei boolean false und bei nicht primitiven Typen einfach null.

Verwendung von @SpyBean in Spring Boot Unit-Tests

Das ist natürlich unglücklich, wenn wir unser Mock an anderer Stelle injecten. Jede andere Klasse kann ja potenziell auf beliebige Methoden unserer gemockten Klasse zugreifen. Auch in Zukunft… Was nun? Eine (Spoiler: sehr schlechte) Möglichkeit wäre es, einfach alle öffentlichen Methoden zu mocken. Damit haben wir uns quasi eine Fakeimplementierung gebaut, das ist nicht wirklich elegant und in unserem Fall auch überdimensioniert.

Aber auch hier gibt es eine schöne Lösung: Wenn wir kein Mock erzeugen sondern ein Spy, dann erhalten wir als Fallback für nicht gestubbte Methoden die eigentliche Implementierung unserer Klasse. Die Definition läuft komplett analog:

@SpyBean
private MailService mailServiceMock;

Wenn wir jetzt nichts weiter machen, verhält sich diese Spy-Bean exakt genauso wie unser ursprünglicher MailService. Und jetzt können wir anfangen zu stubben: Zum Beispiel könnten wir eine Sonderbehandlung für bestimmte Empfängeradressen hinterlegen und für alle anderen das ursprüngliche Verhalten beibehalten. Nicht sonderlich sinnvoll, da dann ja doch wieder Mails verschickt werden...

Unterschiede zwischen @MockBean und @SpyBean

Ok, ich gebe zu, das ist jetzt etwas verwirrend. Wo genau liegt also der Unterschied zwischen @MockBean und @SpyBean? Beide treten nach außen genauso auf, wie die gemockte/gespyte Klasse. Beim Mock ist das Verhalten, wie wenn ich in meiner Entwicklungsumgebung eine Methode überschreibe: Ich erhalte einen leeren Methodenbody mit dem Default des Datentyps als Rückgabe. Beim Spy würde der Aufruf an die Superklasse zurückgegeben werden.

Worin sie sich nicht unterscheiden ist die Tatsache, dass sich alle Methoden stubben, also auch mit eigener Logik überschreiben lassen. Damit lassen sich bereits viele Problemstellungen lösen. Aber Mockito kann noch mehr!

Mockito.verify in Spring Boot Unit-Tests verwenden

Wir haben es also erfolgreich geschafft, unseren MailService so von der Außenwelt abzukapseln, dass aus unseren Unit-Tests keine Mails verschickt werden. Ziel erreicht. Aber dennoch ist es eine fachliche Anforderung, dass nach der Registrierung eines neuen Users eine Mail mit einem Initialpasswort verschickt wird.

Auch für diesen Anwendungsfall bietet uns Mockito ein sehr elegantes Feature in der API. Mit der statischen Methode verify können Interaktionen mit dem Mock oder Spy überprüft werden. Beispiel gefällig?

verify(mailServiceMock, times(1)).sendMail(anyString(), anyString(), anyString());

Das schöne an modernen APIs ist, dass man eigentlich direkt lesen kann, was sie tun ;-) In unserem Fall verifiziert die Zeile, dass auf dem Objekt genau ein Mal eine bestimmte Methode mit beliebigen Parametern aufgerufen wird. Andernfalls wird eine Exception geworfen, die den Test fehlschlagen lässt. Keine Angst, in diesem Fall werden ausreichend Informationen zum Debugging geliefert.

Nicht nur die genaue Anzahl der Ausführungen lässt sich verifizieren, es stehen noch weitere Möglichkeiten zur Verfügung: never(), atLeastOnce(), atLeast(…) und atMost(…). Einige Beispiele finden sich unter https://javadoc.io/static/org.mockito/mockito-core/3.9.0/org/mockito/Mockito.html#at_least_verification

Selbst komplexe Szenarien lassen sich mit verify sehr elegant testen: Soll ein Geschäftsfall mehrere Mails an unterschiedliche Empfänger auslösen, lässt sich das mit jeweils einer Codezeile verifizieren. Sogar der Mailtext könnte durchsucht und abgeglichen werden.

Objekteigenschaften mit Mockito matchen

Bisher haben wir uns bei den Parametern auf einfache Strukturen beschränkt. Jetzt könnte es aber auch sein, dass wir Empfänger, Betreff und Mailtext nicht als einzelne Strings geliefert bekommen, sondern in einem Objekt.

@Service
public class MailService {
     public long sendMail(MailData mailData) {
          /* … implementation … */
     }

     public static class MailData {
          private String recipient;
          private String subject;
          private String text;
         
          /* getter/setter */
     }
}

Sieht doch gleich besser aus, oder? Nur beim Stubben oder beim Matchen des Methodenaufrufs für verify bekommen wir jetzt ein Problem. Zum Abgleich wird jetzt die equals-Methode der Klasse verwendet. Damit können wir leider nicht mehr so fein filtern, um zum Beispiel die Aufrufe mit einem bestimmten Empfänger zu berücksichtigen.

Aber wir sind natürlich die ersten mit dieser Problemstellung, also gibt es dafür auch eine Lösung. Das Zauberwort heißt argThat und wird von Mockito mitgebracht, ist also kein Spring Boot Mechanismus. Analog zu den schon gesehenen eq(…) und anyString() ist argThat(…) ein Argument Matcher. Im Gegensatz zu eq kann argThat aber mit einem Lambda aufgerufen werden:

verify(mailServiceMock).sendMail(argThat(mailData -> mailData.getSubject().equals(„Hello world“)));

So können wir „in die Objekte“ hineinschauen, wenn das Lambda true liefert, wird der Stub angelaufen oder für verify berücksichtigt.

Alternativen zu Mockito in Spring Boot

Natürlich ist auch Mockito keine Silver Bullet um alle unsere Probleme rund um Tests und externe Abhängigkeiten in Spring Boot zu lösen.

Manchmal kann es tatsächlich mehr Sinn machen, eigene Fake-Implementierungen zu schreiben und diese mit @Primary die eigentliche Bean überschreiben zu lassen. Falls der Mockingaufwand zu hoch wird, oder die zu mockende Klasse Seiteneffekte hat (ja, böse, aber manchmal hat man da ja leider keinen Einfluss drauf…) tut man sich mit einer Fake-Implementierung leichter.

Eine Ebene nach außen können wir noch mit Wiremock gehen. Die Idee ist ähnlich wie bei Mockito, jedoch werden hier nur Http Calls gemockt. Das Prinzip ist im Endeffekt genauso: Wir definieren, für welche URLs und welche Payloads (und Header, etc…) welches Ergebnis geliefert werden soll. Das hat den Vorteil, dass abgesehen von der Konfiguration von Wiremock kein weitere Code in unseren Tests steht, das System also noch mehr Black Box bleibt.

Getting Started with Spring Boot & Mockito

Durch die schnelle Einbindung und die nahtlose Integration lässt sich Mockito wirklich einfach ausprobieren. Also am besten gleich loslegen und die Grenzen austesten!

Wenn Du Fragen zu Mockito oder Spring Boot allgemein hast, schreib doch gerne einen Kommentar unter diesem Artikel oder wende Dich direkt an info@mischok.de

Melde Dich unten direkt zu unserem Newsletter an, um keine Neuigkeiten mehr zu verpassen!

Spring-Boot News

Bleibe auf dem aktuellsten Stand mit unseren kostenlosen Spring-Boot updates. So wirst Du direkt informiert, wenn wir einen neuen Artikel veröffentlichen.
Kein Spam, kein Bullshit, nur Spring-Boot Insider-Wissen, versprochen.

Noch keine Kommentare vorhanden.

Was denkst du?

© 2020 Mischok