Start / Seminare / Java Testing mit JUnit, Mockito und Testcontainers

Modul

Datenbank- und Spring-Boot-Integrationstests

4 Kapitel in diesem Modul-Video · Laufzeit

Vertonung mit synthetischer Stimme · Inhalte redaktionell verantwortet · © 2026 HECKER CONSULTING, alle Rechte vorbehalten

Transkript

Der gesprochene Text dieses Moduls zum Mitlesen, Überfliegen und Durchsuchen. Ein Klick auf einen Zeitstempel springt an die Stelle im Video.

Datenbank- und Spring-Boot-Integrationstests

0:00 Jetzt wird es konkret. Der Container läuft, und in diesem Modul bringen wir die Anwendung dazu, ihn auch zu benutzen — von der Datenbankmigration bis zum HTTP-Endpunkt. Der Satz auf der Folie beschreibt die Falle, die dabei überall lauert: Der teuerste Integrationstest ist der, der beim zweiten Lauf etwas anderes sagt als beim ersten.

0:20 Reproduzierbarkeit ist bei Integrationstests keine Fleißaufgabe, sondern die Grundvoraussetzung. Wir sehen uns deshalb genauso gründlich an, wie man Daten aufräumt, wie man sie herstellt.

Datenbank- und Spring-Boot-Integrationstests

0:31 Zweites Modul des dritten Tages. Wir testen zuerst gegen die echte Datenbank, dann die Persistenzschicht selbst, und schließen mit Spring Boot und der Frage, wie viel Anwendung ein Test wirklich braucht. Das letzte Kapitel entscheidet über die Laufzeit Ihrer gesamten Testsuite.

Gegen die reale Datenbank testen

0:47 Beginnen wir mit der Reihenfolge, die im Test genauso gilt wie in Produktion: erst die Migration, dann die Daten, dann der Test. Ein Datenbank-Integrationstest startet dieselbe Datenbank wie die Produktion, führt die Schema-Migrationen aus und arbeitet danach auf einem definierten Datenbestand. Diese drei Schritte in dieser Reihenfolge — das ist der ganze Aufbau.

1:09 Neu gegenüber allem, was Sie bisher kennen, ist ein Detail mit Konsequenzen: Die Verbindungsdaten stehen erst nach dem Start fest. Port, Benutzer und Passwort kommen vom Container. Das bedeutet, dass keine Konfigurationsdatei sie vorher wissen kann — und genau daran scheitern die meisten ersten Versuche. Achten Sie auf die Richtung, in die die Information fließt. Der Container wird gestartet, und danach fragen Sie ihn nach Adresse, Benutzer und Passwort. Nicht umgekehrt.

1:37 Wer stattdessen eine feste JDBC-Adresse in der Testkonfiguration hinterlegt, hat entweder Glück oder einen kaputten Testlauf — meistens Letzteres, sobald zwei Läufe gleichzeitig stattfinden. Ein zweiter Punkt, den ich schon gestern erwähnt habe und der hier besonders zählt: das feste Image-Tag. Bei einer Datenbank entscheidet die Hauptversion über Dialekt und Verhalten, und die möchten Sie nicht zufällig wechseln.

2:02 Das ist einer der stärksten Gründe für Container-Tests überhaupt. Eine Migration ist Produktionscode — sie verändert produktive Daten, und sie läuft genau einmal. Trotzdem wird sie in vielen Projekten nie getestet, weil das Testschema anders entsteht als das echte. Wenn Ihre Migrationen im Test gegen einen frischen Container laufen, prüfen Sie mit jedem Testlauf den kompletten Weg von leer bis aktuell.

2:24 Eine nicht abwärtskompatible Änderung fällt dann im Pull Request auf statt im Wartungsfenster. Das ist ein Sicherheitsgewinn, den man kaum überschätzen kann. Vier Quellen, und die Reihenfolge geht von technisch zu fachlich. Die Migration allein liefert ein leeres Schema — sauber, aber jeder Test legt alles selbst an. Ein Testdatenskript liefert einen gemeinsamen Grundbestand und wird mit der Zeit unübersichtlich. Ein Builder im Test ist fachlich lesbar und liefert genau das Nötige.

2:54 Und die letzte Zeile ist bewusst eindeutig: Ein Abzug aus Produktion ist ungeeignet — personenbezogen, instabil und viel zu groß. Die Fußzeile fasst das Prinzip zusammen: Der beste Bestand ist der kleinste, aus dem sich die geprüfte Regel noch ableiten lässt. Der erste Punkt ist die feste Adresse in der Testkonfiguration, die wir eben besprochen haben.

3:15 Der zweite ist der, der die Migrationen wertlos macht: Sie laufen nur in Produktion, im Test wird das Schema generiert. Dann testen Sie gegen ein Schema, das es nirgends gibt. Der dritte ist ein Wachstumsproblem — das Testdatenskript wächst, bis niemand mehr weiß, welcher Test welche Zeile braucht, und dann traut sich keiner mehr, etwas zu löschen.

3:35 Und der vierte ist das Image ohne festes Tag, das still die Hauptversion wechselt.

Repository- und Persistence-Tests

3:41 Weiter zur Persistenzschicht. Und die Leitfrage dieses Kapitels ist eine, die viele Testsuiten nie beantwortet haben: Wer räumt eigentlich auf? Ein Repository-Test prüft das Zusammenspiel von drei Dingen: Objektmodell, Mapping und SQL. Er ist die Ebene, auf der Constraints sichtbar werden, Kaskaden, das Ladeverhalten und die Transaktionsgrenzen.

4:02 All das sieht kein Unit-Test, weil dort keine Datenbank beteiligt ist — und all das führt in Produktion regelmäßig zu Überraschungen. Diese Ebene ist deshalb ihr Geld wert, auch wenn sie langsamer ist. Und sie ist die Ebene, auf der die Frage nach dem Aufräumen zum ersten Mal wirklich weh tut. Spring rollt Transaktionen in Tests standardmäßig zurück. Das ist bequem: Die Datenbank bleibt sauber, ohne dass Sie eine Zeile Aufräumcode schreiben.

4:29 Es hat aber einen blinden Fleck — genau der Fall, in dem wirklich commitet wird, wird nicht geprüft. Und ein Constraint, der erst beim Commit greift, bleibt unentdeckt. Dazu kommt eine Einschränkung, die viele überrascht: Bei einem echten HTTP-Zugriff greift der Rollback nicht, weil Server und Client in getrennten Transaktionen arbeiten.

4:49 Wer dort schreibt, muss selbst aufräumen — sonst erbt der nächste Test die Daten. Drei Wege, drei Kompromisse. Der Rollback ist schnell und braucht keinen Aufräumcode, prüft aber kein echtes Commit. Tabellen zu leeren prüft echtes Verhalten, verlangt aber, die Reihenfolge der Fremdschlüssel zu beachten — und die ändert sich mit dem Schema.

5:10 Ein eigener Container je Testklasse ist vollständig isoliert und kostet Startzeit. Es gibt hier keine richtige Antwort, nur eine bewusste. Und die Fußzeile sagt, wohin diese Entscheidung gehört: in die Teststrategie aus dem ersten Modul. Dort steht sie einmal, statt in jeder Testklasse neu erfunden zu werden. Bevor jemand die Container-Tests aus der Pipeline nimmt, lohnen vier Hebel. Ein geteilter Container je Testklasse spart den Start je Methode — das ist der größte.

5:39 Wiederverwendete Anwendungskontexte in Spring vermeiden das erneute Hochfahren der Anwendung; auch das kostet sonst Sekunden je Klasse. Test-Slices laden nur die Schicht, um die es geht. Und der vierte Punkt ist der, mit dem man anfangen sollte: messen. In fast jeder Testsuite gibt es eine einzelne Klasse, die allein die Hälfte der Laufzeit trägt. Diese eine zu finden, ist wirksamer als jede allgemeine Optimierung.

6:05 Der erste Punkt ist der Klassiker: Tests hängen an der Reihenfolge, weil einer Daten hinterlässt. Das fällt oft erst auf, wenn die Reihenfolge sich ändert oder parallel gelaufen wird. Der zweite betrifft feste Schlüssel — beim zweiten Lauf kollidiert der Test mit sich selbst. Der dritte ist der blinde Fleck des Rollbacks, den wir eben besprochen haben.

6:24 Und der vierte ist besonders unangenehm, weil er erst in Produktion auftritt: Verzögertes Nachladen funktioniert im Test, weil die Transaktion noch offen ist — im Betrieb ist sie es nicht.

Spring Boot mit realer Infrastruktur

6:36 Zum Schluss die angenehme Nachricht: Spring nimmt Ihnen die Verbindung zwischen Container und Anwendung inzwischen komplett ab. Das Prinzip ist bestechend einfach. Sie markieren einen Container mit einer Annotation, und Spring erkennt daran, um welchen Dienst es sich handelt und wie die zugehörige Autokonfiguration versorgt werden muss.

6:54 Keine Eigenschaften, keine Adressen, keine Zwischenschritte. Nötig ist dafür eine eigene Abhängigkeit für die Testcontainers-Unterstützung. Für alles, wofür es keine fertige Anbindung gibt, bleibt der klassische Weg über dynamisch gesetzte Eigenschaften — der funktioniert weiterhin und ist gut zu kennen, denn bei einem selbstgebauten Container brauchen Sie ihn.

7:15 Das ist der ganze Aufwand. Eine Annotation für den Testlauf, eine für den Container, eine für die Verbindung — und Spring startet die Anwendung gegen diese Datenbank. Wenn Sie Testaufbauten aus älteren Projekten kennen, in denen zehn Eigenschaften von Hand gesetzt wurden, ist das ein spürbarer Fortschritt. Beachten Sie auch die Angabe für den Webbereich: Hier wird ein echter Server auf einem zufälligen Port gestartet.

7:39 Damit lässt sich der Endpunkt über HTTP aufrufen — und das ist etwas anderes, als die Methode direkt zu rufen. Diese Tabelle ist die Laufzeit-Tabelle Ihrer Testsuite. Ganz oben der schmalste Ausschnitt: nur die Persistenzschicht, mit Rollback. Darunter nur die Weboberfläche, ohne Datenbank. Dann die ganze Anwendung ohne Server, und ganz unten die ganze Anwendung mit echtem Server. Die Fußzeile nennt beide Kosten des Wachstums: langsamer und unschärfer.

8:07 Der zweite Teil wird oft übersehen — ein Test über die ganze Anwendung sagt Ihnen im Fehlerfall nur, dass irgendwo etwas nicht stimmt. Deshalb ist der kleinste passende Ausschnitt nicht nur schneller, sondern auch diagnostisch besser. Es lohnt sich, genau zu sagen, was ein solcher Test zusätzlich prüft: die Serialisierung, die Statuscodes und die Fehlerbehandlung am Rand der Anwendung.

8:30 Das sind Dinge, die eine direkte Methodenprüfung nie sieht. Der Preis steht im zweiten Punkt — der Aufruf läuft in einem anderen Thread, also greift kein Rollback. Für einzelne Bestandteile im Kontext gibt es weiterhin Mock- und Spy-Annotationen, die Mockito einsetzen. Und meine Empfehlung zum Maß: Ein solcher Test je Anwendungsfall genügt.

8:51 Alles Weitere gehört eine Ebene tiefer, wo es schneller und genauer ist. Der erste Punkt ist der häufigste Grund für zehnminütige Testläufe: Die große Annotation steht auf jeder Testklasse, auch dort, wo eine Schicht gereicht hätte. Der zweite ist subtil und teuer — jede Testklasse setzt andere Eigenschaften, und damit kann Spring den Anwendungskontext nicht wiederverwenden; er wird jedes Mal neu hochgefahren.

9:15 Der dritte ist die vergessene Verbindungsannotation: Der Container läuft, die Anwendung greift ins Leere. Und der vierte ist der Rollback, der stillschweigend ausbleibt, und die Daten, die deshalb liegen bleiben.

Übung

9:28 In der Übung gehen wir durch alle drei Schichten — von der Migration bis zum HTTP-Aufruf — und vergleichen am Ende die Laufzeiten. Krautgut nutzt Flyway, und die Migration mit der Nummer sieben fügt der Bestellung eine Spalte für das Lieferfenster hinzu. Ihre Aufgabe ist, die Anwendung schichtweise abzusichern — und dabei je Prüfung den kleinstmöglichen Ausschnitt zu wählen.

9:50 Das Erfolgskriterium ist bewusst zugespitzt: genau ein Test ruft den Bestell-Endpunkt über HTTP auf. Genau einer. Die Versuchung, es bei mehreren zu belassen, ist groß, und die Laufzeit zeigt Ihnen am Ende, warum das keine gute Idee wäre. Achten Sie außerdem darauf, dass die Migration mit der neuen Spalte vor dem Testbestand läuft — sonst kennt der Bestand sie nicht.

10:12 Die ersten vier Schritte gehen von unten nach oben: Migration, Repository, Anwendung mit Container, Anwendung mit Server. Wichtig ist die Reihenfolge, denn jede Schicht baut auf der darunter auf — und wenn die Migration nicht sauber läuft, brauchen Sie mit dem Rest gar nicht anzufangen. Schritt fünf ist wieder der, der aus einer Übung eine Erkenntnis macht: die Laufzeiten der drei Ebenen vergleichen und die langsamste begründen.

10:37 Erfahrungsgemäß finden Sie dabei mindestens einen Test, der eine Ebene zu hoch angesiedelt ist. Der erste Punkt ist ein Reihenfolgeproblem, das leicht passiert: Die neue Migration läuft, aber der Testbestand wurde vorher angelegt und kennt die neue Spalte nicht. Der zweite ist der HTTP-Test, dessen Daten kein Rollback mehr entfernt — hier brauchen Sie eigenen Aufräumcode.

10:59 Der dritte ist die Gewohnheit, für jede Prüfung die ganze Anwendung zu starten, auch für reine Repository-Fragen. Und der vierte ist der unsichtbare Laufzeitfresser: unterschiedliche Eigenschaften je Testklasse, und der Anwendungskontext startet immer wieder neu.

Lieber mit Trainer? Dieses Modul ist Teil unserer Team-Schulungen — mit Übungen, Ihrem eigenen Code und Fragen, die ein Video nicht beantwortet. Mehr erfahren →