Start / Seminare / Java Testing mit JUnit, Mockito und Testcontainers
Modul
Testcontainers 2 einrichten und verwenden
4 Kapitel in diesem Modul-Video · Laufzeit
Transkript
Der gesprochene Text dieses Moduls zum Mitlesen, Überfliegen und Durchsuchen. Ein Klick auf einen Zeitstempel springt an die Stelle im Video.
Testcontainers 2 einrichten und verwenden
0:00 Willkommen zum dritten Tag. Heute geht es um Realismus. Zwei Tage lang haben wir Abhängigkeiten ersetzt — mit gutem Grund und mit klaren Grenzen. Jetzt drehen wir das um und starten echte Dienste: eine echte Datenbank, einen echten Broker, eine echte HTTP-Gegenstelle. Das war früher aufwendig und ist es heute nicht mehr, und genau diese Verschiebung hat die Art verändert, wie wir testen.
0:23 Der Satz auf der Folie fasst das Problem der Alternative zusammen: Eine In-Memory-Datenbank sagt Ihnen, ob der Code läuft — nicht, ob er in Produktion läuft.
Testcontainers 2 einrichten und verwenden
0:33 Der dritte Tag hat drei Module. Heute richten wir Testcontainers ein, testen morgen früh gegen die echte Datenbank samt Spring Boot und bauen am Nachmittag die vollständige Testumgebung mit mehreren Diensten, Pipeline und der Frage, was unsere Tests eigentlich wert sind.
Warum echte Container statt Attrappen
0:49 Fangen wir mit der Begründung an. Realismus ist billig geworden — aber er ist nicht kostenlos, und die Kosten sollte man kennen, bevor man alles umstellt. Testcontainers startet für die Dauer eines Testlaufs echte Dienste als Container und räumt sie danach wieder weg. Stellen Sie sich einen Messestand vor: Am Morgen wird er aufgebaut, am Abend abgebaut, und es bleibt nichts stehen.
1:12 Ihr Test spricht dabei mit derselben Software wie die Produktion, nur kurzlebig und auf einem zufällig vergebenen Port. Voraussetzung ist eine Docker-kompatible Container-Laufzeit — welche genau, ist Testcontainers gleichgültig, es kann Docker sein, Podman oder eine entfernte Umgebung. Wichtig ist nur, dass sie da ist, auch auf dem Build-Agenten.
1:32 Diese Gegenüberstellung zeigt den Kern. Links die In-Memory-Datenbank: startet in Millisekunden, spricht aber einen eigenen SQL-Dialekt. Rechts der Container: startet in Sekunden und spricht den Dialekt der Produktion. Die entscheidende Zeile ist die letzte. Links heißt grün: syntaktisch möglich. Rechts heißt grün: so wird es laufen. Das ist ein qualitativer Unterschied, kein gradueller.
1:57 Und er wird immer dann teuer, wenn eine Migration in Produktion scheitert, die im Test problemlos durchlief — weil sie dort gegen etwas anderes lief als gedacht. Das Wort still ist hier das entscheidende. Attrappen liegen nicht mit einer Fehlermeldung daneben, sondern mit grünem Ergebnis. Typen, Indizes und Constraints unterscheiden sich zwischen Datenbanken. Transaktionsverhalten und Isolationsstufen sind nicht austauschbar.
2:23 Migrationen mit datenbankspezifischem SQL laufen in der Attrappe gar nicht erst — also wird das SQL im Test übersprungen und in Produktion zum ersten Mal ausgeführt. Und der letzte Punkt geht in beide Richtungen: Die Attrappe erlaubt manches, was die Produktion verbietet, und verbietet anderes, was dort erlaubt wäre. Beide Richtungen kosten.
2:44 Damit wir nicht einseitig werden: Container kosten. Der Start dauert Sekunden je Image, die Tests laufen in Zehnteln statt Millisekunden, und Sie brauchen eine Container-Laufzeit — auch dort, wo Ihre Pipeline läuft. Der entscheidende Satz steht in der Fußzeile: Die Startzeit fällt einmal je Testklasse an, nicht je Test. Das ist der Hebel für die gesamte Gesamtlaufzeit, und er hängt an einer einzigen Zeile Code, die wir uns im dritten Kapitel ansehen.
3:10 Wer sie falsch setzt, bekommt einen Testlauf, der vierzig Minuten dauert statt vier. Der erste Punkt ist genau dieser Hebel, falsch gesetzt. Der zweite ist der Klassiker beim Sprung von lokal in die Pipeline: Auf dem Build-Agenten fehlt die Container-Laufzeit, und alle Container-Tests werden übersprungen — oft mit grünem Gesamtergebnis.
3:30 Der dritte ist eine Frage des Maßes: Container-Tests ergänzen Unit-Tests, sie ersetzen sie nicht. Und der vierte ist ein Reproduzierbarkeitsproblem mit Ansage — ein Image ohne festes Tag ändert sich, ohne dass jemand etwas commitet. Dann verhält sich Ihr Testlauf plötzlich anders, und im Verlauf steht nichts.
Einrichtung und Containerdefinition
3:49 Jetzt zur Praxis. Und wer Testcontainers aus einem älteren Projekt kennt, sollte hier besonders aufmerksam sein — Version 2 hat Namen und Pakete umgestellt. Das ist die wichtigste Neuerung für alle, die schon mit Testcontainers gearbeitet haben. In Version 2 tragen alle Module das Präfix testcontainers- im Artefaktnamen, und die Containerklassen sind in modulspezifische Pakete umgezogen.
4:13 Aus dem Modul für PostgreSQL wird also das Artefakt testcontainers-postgresql, und die Klasse liegt nicht mehr im gemeinsamen Sammelpaket, sondern in ihrem eigenen. Das ist mehr Ordnung und eine einmalige Umstellungsarbeit. Und noch eine Änderung, die Konsequenzen hat: Die direkte Unterstützung für JUnit 4 ist mit Version 2 entfallen.
4:33 Vier Zeilen, vier Umstellungen. Die ersten beiden betreffen Artefaktname und Paket — das ist Suchen und Ersetzen, aber es muss vollständig sein. Die dritte ist die inhaltlich schwerwiegendste: Wer noch JUnit-4-Tests mit Testcontainers betreibt, muss die zuerst nach Jupiter migrieren. Und die vierte ist eine Falle für Copy-and-Paste aus älteren Beispielen im Netz. Die Fußzeile beruhigt an einer Stelle: Die Gruppe bleibt dieselbe.
5:00 Geändert haben sich Artefaktname und Paket, nicht der Hersteller — wer nach der Gruppe sucht, ist also nicht ganz falsch. Auch hier wieder eine BOM, genau wie bei JUnit, und aus demselben Grund: eine Version für alles. Das ist bei Testcontainers besonders wichtig, weil Sie schnell drei oder vier Module im Projekt haben — Kern, Jupiter-Integration, Datenbank, vielleicht noch einen Broker.
5:23 Ohne BOM ziehen zwei davon irgendwann unterschiedliche Kernversionen, und die Fehlermeldung dazu ist wenig hilfreich. Danach kommen die eigentlichen Module ohne Version dazu, wie in der Fußzeile beschrieben. Für den Anfang genügen zwei: die Jupiter-Integration und das Modul für die Technologie, die Sie brauchen. Nicht für jede Software gibt es ein fertiges Modul. Dann nehmen Sie den allgemeinen Container und beschreiben ihn selbst: welches Image, welcher Port, wann er als bereit gilt.
5:52 Genau diese dritte Angabe ist die wichtige — ohne sie startet Ihr Test möglicherweise, bevor der Dienst antwortet. Die letzte Zeile zeigt das Prinzip, das Sie sich merken sollten: Sie fragen den Container nach dem Port. Der Container-Port ist im Inneren immer derselbe, nach außen wird er zufällig gemappt. Deshalb steht nirgends in Ihrem Test eine feste Portnummer.
6:14 Der erste Punkt trifft jeden beim Umstieg: Der alte Paketname steht noch im Import, und die Klasse wird nicht gefunden. Das ist schnell behoben, sobald man weiß, dass sich das Paket geändert hat. Der zweite ist die fehlende BOM mit ihren Versionskonflikten. Der dritte ist der feste Port, der auf Ihrem Rechner mit einem lokalen Dienst kollidiert — und dann verhält sich Ihr Testlauf anders als der aller anderen.
6:38 Und der vierte ist der übernommene JUnit-4-Test mit seiner alten Regel-Schreibweise, der sich schlicht nicht mehr übersetzen lässt.
Lebenszyklus und Fehleranalyse
6:46 Und jetzt zu der Zeile, die ich vorhin angekündigt habe. Ein einziges Schlüsselwort entscheidet darüber, ob Ihr Testlauf vier oder vierzig Minuten dauert. Die Jupiter-Integration braucht zwei Annotationen: eine an der Klasse, die die Erweiterung aktiviert, und eine am Feld für jeden Container. Und dann kommt das Entscheidende: Ist das Feld statisch, startet der Container einmal je Testklasse und wird von allen Tests geteilt.
7:11 Ist es ein Instanzfeld, startet er vor jeder einzelnen Testmethode neu. Bei zwanzig Tests und fünf Sekunden Startzeit ist das der Unterschied zwischen fünf Sekunden und hundert. In beiden Fällen wartet Testcontainers vor dem ersten Test, bis die Wartebedingung erfüllt ist. Diese Kette zeigt, wo die Zeit hingeht. Das Ziehen des Images passiert nur beim ersten Mal — danach liegt es lokal, und in der Pipeline hängt es am Cache.
7:36 Das Starten dauert Sekunden. Der dritte Schritt ist der, den man beeinflussen kann und der am häufigsten falsch konfiguriert ist: Warten, bis der Dienst wirklich bereit ist. Erst danach laufen die Tests, und am Ende wird aufgeräumt. Wichtig ist, dass dieses Aufräumen verlässlich passiert, auch wenn ein Test abbricht — dafür sorgt Testcontainers mit einem eigenen Aufräumdienst im Hintergrund.
8:00 Die Voreinstellung steht ganz oben: Testcontainers wartet, bis der erste veröffentlichte Port auf Verbindungen hört. Das genügt oft und manchmal nicht — ein offener Port heißt nicht, dass die Datenbank ihre Initialisierung abgeschlossen hat. Für solche Fälle gibt es die anderen Zeilen: auf eine Logzeile warten, auf einen HTTP-Endpunkt, auf den eingebauten Healthcheck des Images, oder auf ein Kommando im Container selbst.
8:24 Die Fußzeile nennt die Voreinstellung mit ihrer Zeitgrenze — sechzig Sekunden. Wenn ein Test nach genau einer Minute abbricht, wissen Sie jetzt, woher die Zahl kommt. Das ist der praktischste Rat dieses Moduls. Ein Container, der nicht startet, sagt Ihnen im eigenen Log warum — meistens sehr deutlich, oft in der letzten Zeile vor dem Abbruch.
8:45 Testcontainers gibt Ihnen zwei Wege dorthin: die Ausgabe nachträglich abholen oder sie live mitschreiben lassen. Ohne diesen Blick sehen Sie nur eine abgelaufene Wartezeit, und die sagt nichts über die Ursache. Bei Datenbanken ist es fast immer eine fehlende Umgebungsvariable oder ein Berechtigungsproblem — beides steht im Log und nirgendwo sonst.
9:06 Der erste Punkt ist die Zeile, um die es in diesem Kapitel ging: Feld nicht statisch, Datenbank startet je Test. Der zweite ist die vergessene Annotation an der Klasse — dann wird der Container nie gestartet, und der Test scheitert mit einer Verbindungsmeldung, die in die Irre führt. Der dritte ist eine feine Sache: Die Wartebedingung passt auf eine Logzeile, die schon beim Hochfahren erscheint. Dann wartet der Test auf nichts.
9:30 Und der vierte ist der Hinweis aus dem fünften Modul — die Jupiter-Integration von Testcontainers ist für serielle Ausführung gedacht.
Übung
9:38 In der Übung binden wir den ersten Container ein und messen den Unterschied, über den wir eben gesprochen haben — statt ihn zu glauben. Ihre Aufgabe ist, die Repository-Tests von Krautgut gegen echtes PostgreSQL laufen zu lassen statt gegen eine Datenbank im Speicher. Das ist der handwerkliche Teil. Der interessante Teil steht im Lernziel: Sie sollen die Kosten der beiden Lebenszyklusvarianten selbst messen. Nicht abschätzen, nicht nachlesen — messen.
10:05 Und der Hinweis ist eine Regel, die ich Ihnen für jedes Projekt mitgebe: Das Image trägt ein festes Tag. Damit tut Ihr Testlauf morgen dasselbe wie heute, und wenn sich etwas ändert, steht die Änderung im Verlauf. Die ersten beiden Schritte sind Einrichtung. Schritt drei bringt den ersten grünen Lauf — und die erste Laufzeit, die Sie notieren.
10:26 Schritt vier ist der eigentliche Erkenntnisgewinn: dasselbe mit einem Instanzfeld, und die Laufzeit erneut messen. Der Unterschied zwischen diesen beiden Zahlen ist das Argument, mit dem Sie im Projekt jede Diskussion über Container-Laufzeiten führen können. Schritt fünf schaut ins Log und setzt eine passende Wartestrategie — damit haben Sie den Ablauf aus dem dritten Kapitel einmal in der Hand gehabt statt nur gesehen.
10:49 Der erste Punkt ist der alte Paketname, der beim Umstieg auf Version 2 hängen bleibt. Der zweite ist das Image ohne Tag — es funktioniert heute und überrascht Sie in drei Wochen. Der dritte ist der, der die ganze Übung entwertet: Die Laufzeit wird geschätzt statt gemessen, und dann bleibt der Unterschied eine Behauptung. Nehmen Sie die Zahlen wirklich auf.
11:09 Und der vierte ist ein Aufräumproblem: Ein von Hand gestarteter Container läuft nach dem Testlauf weiter — das merken Sie spätestens, wenn der nächste Lauf den Port nicht mehr bekommt.
Lieber mit Trainer? Dieses Modul ist Teil unserer Team-Schulungen — mit Übungen, Ihrem eigenen Code und Fragen, die ein Video nicht beantwortet. Mehr erfahren →