Spring Boot Testing
In diesem Kapitel lernst du verschiedene Testarten kennen, um eine Spring Boot Applikation auf Herz und Nieren zu prüfen.
Ziele
- Ich kenne die verschiedenen Testarten, um Spring Boot Applikationen zu testen und weiss, bei welchen Testszenarien diese anzuwenden sind.
- Ich kann verschiedene Testarten anwenden und Testfälle dazu schreiben.
- Ich kann Tests selektiv aktivieren/deaktivieren.
Das Demo-Projekt und sein Setup
Damit du den folgenden Erklärungen einfacher folgen und laufenden Code untersuchen kannst, klone bitte das folgende GitHub-Repo und öffne das Projekt in deiner IDE (Branch: master): https://github.com/it-ninjas/springboottesting/tree/master
Das Repo beinhaltet eine laufende Spring Boot Applikation (wen wundert’s ;-)), die Personen verwaltet. Via REST-Schnittstelle können neue Personen erstellt werden (createPerson) und es gibt die Möglichkeit, alle Personen abzufragen (getAllPersons). Damit du möglichst einfach Requests machen kannst, ist ein SwaggerUI vorhanden: http://localhost:8082/swagger-ui/index.html
Datenbank: Die Applikation benötigt eine laufende JDBC Verbindung auf eine Maria-DB mit Namen ’testDB’. Die weiteren Verbindungs-Informationen entnimmst du /src/main/resources/application.properties.
Datenbank Setup
Du kannst entweder eine bestehende Datenbank verwenden oder dann mittels Docker/Podman einen Maria-DB Container hochfahren:- Bestelle/Aktiviere dir die temporären Adminrechte. Als Grund kannst du die Installation von Podman angeben.
- Führe die Podman-Installation gemäss https://github.com/containers/podman/blob/main/docs/tutorials/podman-for-windows.md durch.
Hier ist die Anleitung für Windows verlinkt. Du musst nur das Kapitel Installing Podman durchführen.
- Falls du eine Fehlermeldung im Sinne von “WSL 2 erfordert ein Update der Kernelkomponente.” erhältst, folge diesem Link: https://aka.ms/wsl2kernel und führe mindestens die Schritte 4 und 5 aus. Anschliessend führst du in der Kommando-Zeile
podman machine init
aus.
- Falls du eine Fehlermeldung im Sinne von “WSL 2 erfordert ein Update der Kernelkomponente.” erhältst, folge diesem Link: https://aka.ms/wsl2kernel und führe mindestens die Schritte 4 und 5 aus. Anschliessend führst du in der Kommando-Zeile
- Öffne die Kommando-Zeile (cmd, powershell, etc.).
- Podman starten (muss nach jedem Neustart des Geräts gemacht werden):
podman machine start
- Maria-DB Container erstellen und starten:
- Wechsle ins Projektverzeichnis und erstelle einen Ordner ‘maria-db’:
mkdir maria-db
- Starte den Container unter dem Namen mdb:
podman run -d --name=mdb -p 3306:3306 -e MYSQL_USER=admin -e MYSQL_PASSWORD=saysoyeah -e MYSQL_ROOT_PASSWORD=SQLp4ss -e MYSQL_DATABASE=testDB -v ./maria-db:/var/lib/mysql docker.io/mariadb:latest
- Wenn du bereits eine Maria-DB auf Port 3306 am Laufen hast, musst du diese stoppen oder hier einen anderen Port verwenden: Ändere die erste Zahl 3306 auf einen neuen Port und anschliessend musst du auch die beiden application.properties Dateien anpassen.
- Prüfe, ob der Container läuft mittels:
podman ps -a
- Ab jetzt kannst du den Container mittels
podman stop mdb
undpodman start mdb
stoppen und starten.
- Wechsle ins Projektverzeichnis und erstelle einen Ordner ‘maria-db’:
Die Applikation ist minimalistisch aber mit den wichtigsten Spring-Boot-Layers aufgebaut:
- Wir haben 1 Entity:
Person
- Das
PersonRepo
basiert aufJpaRepository
und definiert keine zusätzlichen Methoden. - Auch der
PersonService
ist sehr kompliziert :-). Wichtig ist aber, dass er die@Component
MyUtilityBean
verwendet. So haben wir auch noch eine Utility-Bean, die wir beim Testen berücksichtigen können. - Der
PersonController
bietet zwei REST API Methoden an:- /persons (liefert alle Personen)
- /createPerson (so kannst du eine neue Person anlegen)
Der PersonController
verwendet den PersonService
, welcher auf das PersonRepo
zugreift, das die Person
-Entity nutzt.
Das Zwiebelprinzip in Reinkultur ;-).
Während du die Doku hier liest, schaust du dir parallel dazu die erwähnten Code-Stellen an, lässt die beschriebenen Tests laufen und versuchst so, die Erkärungen nachzuvollziehen.
Testarten und Best Practices
Wir wollen alle Layers (von der Entity bis zum Controller) testen. Dazu gibt es verschiedene Möglichkeiten und Best-Practices.
Natürlich kannst du jeden Layer mit JUnit/Mockito testen. Das hast du ja bereits früher gelernt.
Dazu nimmst du deine Unit-Under-Test (UUT) und mockst alles, was “darunter” liegt. Je nachdem kann das aber ganz schön mühsam
sein resp. praktisch nicht möglich: Stell dir vor, du willst ein @Repository
mit Mockito testen. Wie kommst du
da an die automatisch durch den Spring-Container generierten Methoden (z.B. findAll()
)? Gar nicht.
Deshalb gibt es Möglichkeiten, je nach Anwendungsfall den Spring Application-Context teilweise oder ganz hochzufahren.
Wollen wir nur den Daten-Teil (Entity und Repo) hochfahren ist das ein @DataJpaTest
.
Hier werden keine Services und auch keine Controller instanziiert.
Wollen wir nur den Controller-Teil (inkl. Security, Filter, Converter) hochfahren, ist das ein @WebMvcTest
.
Es werden keine Services, keine Repos und keine Entities instanziiert.
Bei @DataJpaTest
und @WebMvcTest
sprechen wir von sogenannten Slice-Tests, weil wir nur einen Teil-Bereich (intergrations-)testen.
Natürlich können wir auch den ganzen Spring Application Context hochfahren. Dann verwenden wir @SpringBootTest
.
Für Services gibt es keine spezielle Slice-Test-Annotation oder Umgebung. Da nehmen wir entweder Mockito oder dann @SpringBootTest.
@DataJpaTest, @WebMvcTest und @SpringBootTest fahren den Application-Context (oder zumindest Teile davon) hoch. Das ist langsamer bei der Ausführung, als pure Unit-Tests. Wir werden daher später lernen, nur gewisse Tests zu starten.
Bei @DataJpaTest, @WebMvcTest und @SpringBootTest kann man von ‘Integration-Tests’ sprechen, weil verschiedene Komponenten im Zusammenspiel untersucht werden. Wir könnten nun mit Maven das Failsave Plugin verwenden, das für Integrations-Tests verwendet wird. Das Failsave-Plugin springt in der Maven Phase ‘integraton-test’ an, also nach der Unit-Test-Phase ’test’. Es funktioniert anders als Unit-Tests. Um die Komplexität zu reduzieren, fahren wir in diesem Projekt aber alle Tests als Unit-Tests (Maven Phase ’test’).
Hier nochmals in der Übersicht, welche Testarten sich für welchen Layer eignen:
Layer | Testart(en) | Begründung |
---|---|---|
Entity/Repo | @DataJpaTest | Wir wollen Queries bis auf die DB “runter” testen. Es gibt viel DB-Interaktion, nicht aber Logik. |
Service | Mockito und @SpringBootTest | Services beinhalten die Logik, daher mit Mockito. Services verbinden Repo und Controller, daher @SpringBootTest. |
Controller | @WebMvcTest | Es geht primär darum, Anfragen entgegenzunehmen und zurückzugeben, also Eingabe- und Ausgabe-Formate zu testen. Security etc. können ein Thema sein, nicht aber Logik. |
Utility-Klasse (hier MyUtilityBean) | Mockito | Reine Logik-Tests resp. “tun die Helper-Methoden, was sie sollen?"-Tests. Hinweise: Im Demo-Projekt gibt es keinen Unit-Test für die MyUtilityBean. |
Tests ausführen, SpringBoot-Tests ignorieren
Du kannst alle Tests mit mvn clean test
ausführen.
Evtl. schlägt der Test PersonRepoTestContainerDataJpaTest
fehl. Das hat damit zu tun,
dass bei dir Docker/Podman noch nicht installiert ist. Wir schauen das weiter unten im Abschnitt Testcontainers an.
Wie bereits weiter oben erwähnt, sind @DataJpaTest
-, @WebMvcTest
- und @SpringBootTest
-Tests zeitaufwändig bei der Ausführung.
Deshalb wird auf diese Tests manchmal in einem ersten Testlauf auch verzichtet. Dazu gibt es verschiedene
Wege, der einfachste ist aber so:
|
|
Mit -Dsurefire.excludes=...
kannst du festlegen, welche Unit-Tests ignoriert werden sollen.
Das obige Beispiel bezieht sich auf die Bennenung der Tests im Demo-Projekt: Alle Tests die
WebMvcTest oder DataJpaTest oder SpringBootTest im Klassenamen haben werden so ignoriert.
Service testen mit Mockito
Nun schauen wir an, wie die einzelnen Layers getestet werden können. Starten wir mit dem PersonService
!
Hier die UUT:
|
|
Wir schreiben zuerst Mockito-Tests. Wie das geht, sollte dir bereits bekannt sein. So sieht unser Testaufbau aus:
Damit Mockito funktioniert, verwende die folgende Dependency im pom.xml
|
|
|
|
Wichtige Punkte zum Test:
- Das
PersonRepo
wird gemockt. createPerson()
:- Die
MyUtilityBean
wird gespied, ob sie 2x aufgerufen wird und gleichzeitig wird gecaptured, ob die Bean auch die korrekten Person-Objekte übergeben bekommt.
- Die
Service mit @SpringBootTest testen
Wir testen erneut den PersonService
, jetzt aber mit dem kompletten Application-Context. Auf Mocks
verzichten wir. Es gibt keine spezielle Annotation für Slice-Tests mit Services. Deshalb fahren wir den gesamten
Application-Context hoch:
Wir verwenden eine H2 In-Memory Datenbank.
Verwende die folgenden Dependencies im pom.xml
|
|
|
|
Wichtige Punkte zum Test:
- Zur DB-Konfiguration verwenden wir das application.properties aus /src/test/resources. Wird ein Test gestartet, scannt Spring zuerst die Dateien in /src/test. Da dort ein application.properties vorhanden ist, wird dieses verwenden. Falls nicht, würde in einem zweiten Schritt /src/main gescannt und die entsprechende application.properties Datei verwendet werden.
- Anstelle von
@Spy
(und@Mock
) wird@SpyBean
(und@MockBean
) verwendet. Du kannst jedoch dieselben Assertions und Verifys verwenden.@MockBean
wird eingesetzt, damit der Mock die effektive Bean im Application-Context ersetzt und so überall der Mock verwendet wird.@SpyBean
wird genutzt, um die bestehende Bean innerhalb des Applikation-Contexts auszuspionieren und nicht isoliert zu betrachten. - Für den Test
getAllPersons()
verwenden wir ein spezifisches@Sql
Script data_personservice.sql. - Die Annotation
@DirtiesContext
bewirkt, dass nach dem Test die DB zurückgesetzt wird. Andernfalls hätten wir noch die Daten aus dem vorherigen Test in der DB. - Im Test
createPersons()
verwenden wir einen ArgumentCaptor auf dermyUtilityBean
und zählen, ob auf ihr 2x die MethodeaddPerson()
aufgerufen wird und ob die erste Person auch unseren Testdaten entspricht. - WebEnvironment deaktivieren: Falls du verhindern möchtest, dass das WebEnvironment (u.a. Controller) hochgefahren wird, kannst du die Annoation
@SpringBootTest
erweitern um@SpringBootTest(webEnvironment = WebEnvironment.NONE)
. In diesem Szenario hier wäre das sicher sinnvoll, da wir den Controller sowieso nicht verwenden. Also los, ändere die Annotation!
Controller mit @WebMvcTest testen
Nun testen wir den PersonController
. Dazu fahren wir einen Slice-Test mit @WebMvcTest
. Es werden weder Services, noch Repos, noch Entities hochgefahren.
Deshalb mocken wir den PersonService
:
Verwende die folgenden Dependencies im pom.xml
|
|
Hier die UUT:
|
|
Und hier die Test-Klasse:
|
|
Wichtige Punkte zum Test:
- Der
PersonService
wird gemockt. - Wir verwenden einen
MockMvc
. Damit können wir (REST-)Requests absetzen und die Antworten auswerten.getAllPersons()
mockMvc.perform(get("/persons")).andDo(print()).andExcpect(...)...
: Zuerst wird die Rest Schnittstelle /persons aufgerufen.andDo(print())
gibt die Response in der Konsole aus. Alle weiterenandExpect(...)
arbeiten mit der Reponse.content().string(...)
holt den Body der Reponse und konvertiert ihn in einen String. Danach wird geschaut, ob der Body gewisse Strings enthält.createPerson()
wertet die JSON-Response detailliert aus.objectMapper.writeValueAsString(dto)
: Wir konvertieren das Person-Objekt automatisch nach JSON.- Bei
jsonPath("$.personName")
bezieht sich $ auf das zurückgegebene einzelne Objekt. Erwarten wir eine Liste von Objekten kann über den Index auf ein entsprechendes Objekt zugegriffen werden. Wollen wir z.B. auf das 2te Objekt in der Liste zugreifen, verwenden wir $[1].personName .
- (Tipp am Rande: Falls du trotzdem eine DB verwenden würdest: Es gibt kein automatisches Rollback der Daten nach jedem Test.)
Repo/Entity mit @DataJpaTest testen
Jetzt ist das PersonRepo
inkl. Person
(Entität) und DB dran. Wir fahren den Slice-Test mit @DataJpaTest
.
Es werden nur DB, Entities und Repositories initialisert, keine Services, keine Controller:
Auch hier verwenden wir die H2 In-Memory DB.
Verwende die folgenden Dependencies im pom.xml
|
|
Hier die UUT:
|
|
Und hier die Test-Klasse:
|
|
Wichtige Punkte zum Test:
- Zur DB-Konfiguration verwenden wir das application.properties aus /src/test/resources. Dieses wird zuerst verwendet, weil es vorhanden ist.
@BeforeEach
: vor jedem Test füllen wir die DB mit einer Person ab.- Nach jedem Test müssen wir die DB nicht manuell (oder mit
tearDown()
) resetten oder den Test mit@DirtiesContext
annotieren. Dies passiert bei@DataJpaTest
automatisch.
Testcontainers
Mit Testcontainers kannst du beliebige Umsysteme einbinden. Das System basiert auf Docker/Podman-Containern, die für den Test hochgefahren und initalisiert werden. Der Vorteil ist, dass du für den Test exakt dieselben Umsysteme wie in der Produktion verwenden kannst. Beispiel: Anstelle einer H2 In-Memory-DB können wir nun eine Maria-DB verwenden, wie sie auch in der deployten App genutzt wird. Grundsätzlich kannst du alle Container verwenden, die auf https://hub.docker.com/ zur Verfügung gestellt werden oder die du selber erstellt hast.
Damit Testcontainers funktionieren, musst du zuerst Podman/Docker installieren. Falls noch nicht gemacht, führe die Schritte 1-4 aus im oben beschriebenen Datenbank Setup. Nun hast du Podman installiert und gestartet.
Wir testen erneut das PersonRepo
mit einem @DataJpaTest
.
Verwende die folgenden Dependencies im pom.xml
|
|
|
|
Wichtige Punkte zum Test:
@AutoConfigureTestDatabase( replace = Replace.ANY )
: Wir deaktivieren die Konfiguration aus /src/test/resources/application.properties komplett.@TestPropertySource( properties = {"spring.jpa.hibernate.ddl-auto=create-drop"} )
: Wir übergeben für diese Test-Klasse spezifische Properties, d.h. die DB wird komplett gelöscht und das Schema neu erzeugt.@Testcontainers
: Wird benötigt, damit der Test mit Testcontainers überhaupt funktioniert.- Container erstellen und Config-Daten aus dem gestarteten Container ins application.properties übernehmen:
|
|
@Container
: Wir instanzieren einen Maria-DB Container. Falls der Container lokal noch nicht vorhanden ist im Docker/Podman wird er zuerst heruntergeladen. Hier verwenden wir jeweils die latest-Version von Maria-DB. In einer echten Applikation sollte hier stattdessen eine konkrete Version definiert werden.- Wie du siehst, definieren wir an verschiedenen Orten die benötigten Properties. Standard-Properties werden hier über
@DynamicPropertySource
hinzugefügt. Wir übernehmen aus demtestContainer
die DB-Verbindungs-Properties. Solange wir diese nicht überschreiben, werden Default-Werte zurückgegeben. - Test-spezifische Properties werden über die Klassen-Annotation
@TestPropertySource
gesetzt. Grundsätzlich könntest du aber auch nur mit der DynamicPropertySource arbeiten.
Jetzt bist du dran. Löse die Aufgaben in Spring Boot Testing - Aufgaben!