Häufige Fallstricke beim Testen von Spring Boot-Anwendungen

von Björn Wilmsmann

Dieser Artikel ist gesponsert von Philip (rieckpil) und aus dem Englischen übersetzt von workshops.de - Philip bietet einen umfassenden Online-Kurs zum Thema Testen von Spring Boot Anwendungen (englischsprachig) an.

- Deutsche Übersetzung von Justus Wildhagen -

Zu lernen, wie man eine Spring Boot-Anwendung effektiv testet, kann eine Hürde sein, insbesondere für Anfänger. Ohne grundlegendes Wissen über den Dependency-Injection-Mechanismus von Spring und über die Auto-Konfiguration könnte es dazu kommen, dass wir unserem Test haufenweise Annotations hinzufügen, um die Anwendung zum Laufen zu kriegen. Dieses Herumprobieren kann unser Test Setup vielleicht in manchen Fällen korrigieren, das Endergebnis ist aber in der Regel ein weniger optimales Test Setup. In diesem Blogbeitrag habe ich deshalb die häufigsten Fallstricke gesammelt, die mir in Projekten und bei der Beantwortung von Fragen auf Stack Overflow begegnet sind, wenn es um das Testen von Spring Boot-Anwendungen ging.

Fallstrick 1: @Mock vs. @MockBean

Einer der ersten Fallstricke ist das Mocking von Abhängigkeiten, also den Objekten, von denen unsere zu testende Klasse abhängt. Wenn wir bereits mit Mockito vertraut sind, wissen wir vielleicht, dass wir @Mock verwenden können, um Mocks für unsere Unit Tests zu erstellen.

Wenn wir Tests für unsere Spring Boot-Anwendungen schreiben, müssen wir uns kein spezifisches Mockito-Wissen wieder abgewöhnen. Dennoch müssen wir uns darüber im Klaren sein, welche Art von Test wir schreiben. Funktioniert unser Test mit oder ohne einen Spring TestContext?

Das ist wichtig, weil es bestimmt, ob @Mock oder @MockBean verwendet werden sollte. Während beide Annotations eine “mocked” Version unserer Abhängigkeiten erzeugen, ist @Mock nur für einfache Unit Tests relevant, die ohne einen Spring TestContext funktionieren. In solchen Fällen erstellen wir normalerweise Mocks der Abhängigkeiten und injecten sie über den öffentlichen Konstruktor unserer zu testenden Klasse.

Bei Tests, die mit einem Spring TestContext arbeiten, zum Beispiel beim Verwenden einer Spring Boot Test Slice Annotation oder eines @SpringBootTest, laufen die Dinge anders. Hier wollen wir immer noch die Abhängigkeit unserer zu testenden Klasse mocken. Dieses Mal stellt Spring aber alle unsere Beans zusammen und führt eine Dependency Injection durch. Daher müssen wir eine gemockte Version der Abhängigkeit als Bean innerhalb des Spring TestContextes ersetzen oder hinzufügen.

An dieser Stelle kommt @MockBean ins Spiel. Wir verwenden es über einem Test-Attribut, um Spring Test anzuweisen, eine gemockte Version dieser Bean in unseren TestContext einzufügen.

Ob wir nun @Mock oder @MockBean verwenden, das Mockito-Stubbing-Setup funktioniert für beide gleich.

Der Fallstrick besteht hier eher darin, entweder beide Annotations im selben Test zu vermischen oder eine der Annotations für den falschen Zweck zu verwenden.

Ich habe einen Vergleich der beiden Annotations und wann sie zu verwenden sind, in einem separaten @Mock vs. @MockBean Blogbeitrag behandelt.

Fallstrick 2: Umfangreiche Verwendung von @SpringBootTest

Wenn wir mit dem Testen von Spring Boot-Anwendungen anfangen, werden wir ganz bald über die Annotation @SpringBootTest stolpern. Spring Boot erstellt sogar einen Basistest, der diese Annotation für jedes neue Projekt verwendet, das von start.spring.io generiert wird.

Der Name der Annotation könnte implizieren, dass sie für jeden Spring Boot-Test verwendet und benötigt wird. Das ist aber nicht der Fall.

Wir verwenden @SpringBootTest, wann immer wir einen Integrationstest schreiben wollen, der mit dem gesamten Spring Context funktioniert. Spring Test wird einen TestContext für uns erstellen, der alle unsere Beans enthält (@Component, @Configuration, @Service, etc.).

Das bedeutet, dass wir auch jede externe Infrastrukturkomponente, mit der wir uns verbinden, bereitstellen müssen. Stell dir vor, wir schreiben eine CRUD-Anwendung, die eine Verbindung zu einer Datenbank herstellt. Wir werden nicht in der Lage sein, unsere Repository-Klassen zu erstellen und zu verwenden, wenn es während der Testausführung keine Datenbank gibt, mit der wir uns verbinden können.

Wenn wir nur @SpringBootTest für unsere Tests verwenden würden, würden wir schnell feststellen, dass unsere Test Suite viel länger braucht als einfache JUnit- und Mockito-Tests. Das Starten eines Spring Context führt zu langsameren Testausführungszeiten, weil alles instanziiert und initialisiert werden muss.

Als allgemeine Empfehlung sollten wir versuchen, so viel wie möglich von unserer Implementierung auf einem niedrigeren Testlevel zu testen und zu verifizieren. Das bedeutet, dass ein bestimmter if-Block innerhalb unserer @Service-Klasse mit einem Unit Test getestet werden kann. Darüber hinaus können wir sicherstellen, dass unsere Spring Security-Konfiguration funktioniert, indem wir @WebMvcTest verwenden.

Für Integrationstests, die das Zusammenspiel mehrerer Komponenten überprüfen, oder beim Schreiben von End-to-End-Tests kommt @SpringBootTest ins Spiel.

Mach dich mit den unterschiedlichen Spring Boot Test Slice Annotations vertraut. Darüber hinaus gibt es verschiedene Möglichkeiten zur weiteren Optimierung von @SpringBootTest, derer wir uns bewusst sein müssen.

Fallstrick 3: Überhaupt nicht testen

Ich denke, dieser Fallstrick versteht sich von selbst. Wenn wir unseren Code nicht testen, wie können wir dann jemals sagen, ob er funktioniert?

Wir haben zwar unsere Implementierung manuell überprüft, aber wie können wir sicherstellen, dass kommende Änderungen unser Feature nicht zerstören?

Wenn wir unsere Anwendung nicht testen, werden es unsere User mit Sicherheit tun, und sie werden nicht erfreut sein, wenn sie halbfertige Features vorfinden.

Das Testen hat beim Erlernen von Spring Boot vielleicht nicht oberste Priorität. Das ist in Ordnung, solange wir sicherstellen, dass wir zum Thema Testen zurückkehren, sobald wir uns mit dem Framework wohlfühlen.

Ob wir den Test vor der Implementierung (auch bekannt als Test-driven Development) oder danach schreiben, hängt von deiner persönlichen Vorlieben ab. Ich habe gute Erfahrungen damit gemacht, den Test zuerst zu schreiben, was zu einem durchdachteren Design und kleineren Schritten führt.

Wenn wir den umgekehrten Weg gehen und Tests zu unserem Code hinzufügen, nachdem wir die Implementierung abgeschlossen haben, führt das normalerweise zu nicht so guten Tests. Wir wissen bereits, wie die Implementierung aussieht und sind schnell dazu geneigt, nur ein Minimum zu testen. Hinzu kommt, dass wir eventuell schon spät dran sind, unsere Änderungen zu integrieren und daher wenig Zeit haben, die Implementierung gründlich zu testen.

Das Spring Framework und Spring Boot heben die Wichtigkeit von Testing hervor und ermutigen uns, Tests zu schreiben, indem sie gute Testunterstützung und -Tools anbieten.

Testen ist ein wesentlicher Bestandteil jedes Spring Boot-Projekts, da jedes neue Projekt bereits mit einem grundlegenden Integrationstest und dem “Schweizer Taschenmesser des Testens“ ausgeliefert wird. Josh Long wird uns persönlich besuchen, wenn wir diesen automatisch generierten Test löschen (oder deaktivieren).

Es gibt absolut keine Entschuldigung dafür, keinen Test zu schreiben – es sei denn wir wissen noch nicht, wie es geht. Das können wir aber leicht beheben.

Auf meinem Blog gibt es jede Menge praktische Tipps zum Thema Testing. Ich empfehle, mit dem Artikel Spring Boot unit and integration testing overview zu starten und darüber nachzudenken, sich für den Kurs Testing Spring Boot Applications Primer anzumelden, um den Spring Boot Testerfolg voranzubringen.

Fallstrick 4: Keine Wiederverwendung des Spring TextContext

Dies ist mit dem zweiten Fallstrick verbunden (umfangreiche Verwendung von @SpringBootTest).

Einen neuen Spring TestContext für jede Test-Klasse zu starten ist teuer. Warum also nicht einen bereits gestarteten Spring TestContext cachen?

Genau das macht Spring Test für uns!

Immer wenn wir einen neuen Spring TestContext, einen sliced oder den gesamten Kontext starten wollen, zieht Spring einen bereits gestarteten Kontext für diesen Test in Erwägung. Wenn ein bereits existierender Kontext zur Kontext-Konfiguration der auszuführenden Testklasse passt, wird Spring diesen Kontext wiederverwenden.

Wenn kein passender Cache-Kontext bereits gestartet ist (sprich: ein Cache Miss vorliegt), startet Spring einen neuen und speichert den Kontext anschließend für die Wiederverwendung in anderen Tests.

Wie stellt Spring also fest, ob ein Kontext wiederverwendet werden kann oder nicht, und wie können wir dieses Feature effektiv nutzen?

Stell dir vor, ein Integrationstest aktiviert das Profil integration-test, während ein anderer Test das Profil web-test aktiviert. In einem solchen Fall wird Spring denselben Kontext nicht wiederverwenden, da unsere Konfiguration aufgrund der unterschiedlichen Profile völlig anders aussieht.

Es gibt mehr als zehn Konfigurations- und Setupwerte, die die Einzigartigkeit eines Caches bestimmen. Um diese Leistungsverbesserung effektiv zu nutzen, müssen wir die meisten unserer Spring TestContext-Setups aufeinander abstimmen. Wir sollten mehrere Kontextkonfigurationen vermeiden, insbesondere bei Tests, die mit dem gesamten ApplicationContext arbeiten.

In einem meiner Projekte habe ich die gesamte Bauzeit (Ausführung von mvn verify) von 25 Minuten auf 9 Minuten reduziert, indem ich den Spring TestContext Caching-Mechanismus optimal genutzt habe. Das ist mir gelungen, indem ich die Kontextkonfigurationen für die teuren Integrationstests angepasst habe.

Mach dich mit den unterschiedlichen Konfigurationswerten vertraut und lerne, wie du den TestContext Caching-Mechanismus optimal nutzen kannst.

Fallstrick 5: Vermischung von Up JUnit 4 und JUnit 5

Ein weiterer häufiger Fallstrick beim Testen von Spring Boot-Anwendungen, der zu seltsamen Testergebnissen führt, ist die Vermischung von JUnit 4 und JUnit 5 in derselben Testklasse.

Beim Beantworten von Fragen auf Stack Overflow stoße ich immer wieder auf eine Menge Verwirrung rund um dieses Thema.

Obwohl die erste Version von JUnit 5 im Jahr 2017 veröffentlicht wurde, gibt es immer noch Projekte, die den Vorgänger verwenden (was in Ordnung ist). Da JUnit 5 die Ausführung von JUnit 4-Tests neben JUnit 5-Tests unterstützt, können wir für unsere Projekte während der Migrations-/Übergangsphase beides mischen.

Die gleichzeitige Verwendung von Annotations und APIs aus JUnit 4 und JUnit 5 (JUnit Jupiter, um genau zu sein) wird nicht funktionieren. Es ist ein Entweder-oder. Wir können zwar sowohl JUnit 4- als auch JUnit 5-Tests in unserem Projekt verwenden, aber aufgrund der JUnit Vintage-Engine sollte eine Testklasse sich entweder für Version 4 oder 5 entscheiden.

Die Verwendung von JUnit 4 für MyOrderTest und JUnit 5 für MyPricingServiceTest ist völlig in Ordnung. Der Fallstrick liegt in der Vermischung von APIs und Annotations beider Versionen innerhalb desselben Tests.

Es gibt Tools und Guides, um die Migration zu beginnen und die „low-hanging fruit“ zu konvertieren. Bei der Migration von benutzerdefinierten Runner- oder Rule-Klassen bleibt jedoch noch etwas manueller Aufwand übrig.

Sobald die Migration auf JUnit 5 abgeschlossen ist, empfehle ich, alle JUnit-4-Abhängigkeiten aus dem Projekt zu entfernen. Das hilft, JUnit 4-Überbleibsel mittels eines fehlgeschlagenen Kompilierungsschritts zu identifizieren. Es verringert auch die Wahrscheinlichkeit, dass JUnit 4 (versehentlich) für einen neuen Test wieder eingeführt wird.

Frohes Testen!

Geschrieben von

Mein Name ist Björn. Ich bin freiberuflicher IT Berater, Trainer und Autor. Ich beschäftige mich mit Enterprise Anwendungen und Web Apps. Mein technologischer ...