Close Cookie Preference Manager
Cookie Einstellungen
Wenn Sie auf "Alle Cookies akzeptieren" klicken, stimmen Sie der Speicherung von Cookies auf Ihrem Gerät zu, um die Navigation auf der Website zu verbessern, die Nutzung der Website zu analysieren und unsere Marketingaktivitäten zu unterstützen. Mehr Infos
Unbedingt erforderlich (immer aktiv)
Erforderliche Cookies, um grundlegende Funktionen der Website zu ermöglichen.
Made by Flinch 77
Oops! Something went wrong while submitting the form.
Cookie Einstellungen
May 17, 2021

Testcontainers in Spring Boot – 3 einfache Schritte zur erfolgreichen Einbindung

Containertechnologien, allen voran Docker, haben die IT-Welt in den letzten Jahren umgekrempelt. Der Trend von monolithischen Applikationen zu verteilten Systemen wurde von dieser Entwicklung maßgeblich getrieben. Die neuen Entwurfsmuster erfordern aber auch ein geändertes Vorgehen beim automatisierten Testen der Applikationen. In diesem Artikel zeige ich euch, wie ihr Testcontainers nahtlos in eure Spring Boot Applikation integrieren könnt, wie die Konfiguration funktioniert und welche Grenzen das ganze Konzept hat.

Und mal wieder landen wir beim Thema testen… Im Artikel zu H2 haben wir uns angesehen, wie wir mit einer In-Memory Datenbank wirkliche Unabhängigkeit unserer Tests bewerkstelligen können. Der Traum von Projekt klonen und sofort einmal die Tests laufen lassen wird durch Testcontainers in Spring Boot greifbar! Also geht es im nächsten Projekt gleich mit H2 ans Werk! Eigentlich sind wir es ja gewohnt, aber natürlich läuft nicht alles reibungslos...

H2 und datenbankspezifische Strukturen

Glücklicherweise haben die Kollegen, die das Projekt seinerzeit aufgesetzt haben, ein Datenbankmigrationstool verwendet, zum Beispiel Flyway. Bisherige Praxis war es, dass auf jeder Entwicklungsmaschine eine Instanz von PostgreSQL gestartet wurde. Diese wurde dann sowohl für den lokalen Start der Applikation, wie auch für die Testläufe verwendet.

Neben der Tatsache, dass sich ständig die Testdaten gegenseitig behindern, hat dieses Vorgehen noch einen weiteren großen Nachteil: Die lokale Installation auf den Entwicklermaschinen kostet Zeit und muss aktuell gehalten werden. Im schlimmsten Fall entwickle ich sonst ein Feature gegen eine veraltete Datenbankversion, in der Live Umgebung funktioniert nichts mehr. „Worked on my machine“ in seiner Reinform…

Spricht H2 auch Dialekt?

In vielen Fällen ist es tatsächlich möglich, auch in einem solchen Szenario H2 einzusetzen. Unter Umständen muss in der Konfiguration noch weitergegeben werden, dass sich H2 wie Postgres,  Oracle oder irgendein anderes Datenbanksystem verhalten soll. Compatibility Mode heißt das Zauberwort. Wenn die bestehenden Datenbankmigrationen aus halbwegs standardkonformem SQL bestehen, könnte es sein, dass wir Glück haben.

Mit dem Dialekt ist es aber wie im echten Leben: Irgendwann ist in der Verständigung Schluss – so sehr sich die Gesprächspartner auch anstrengen. Während beim Urlaub im bayerischen Wald zur Not noch Zeichensprache zum Einsatz kommt, streicht H2 irgendwann die Segel. Spätestens wenn die Datenbankmigrationsskripte etwa Definitionen für Trigger enthalten oder spezielle Datentypen für die Spaltendefinitionen verwenden, wird der Aufwand meist zu groß, das Ganze auf H2 lauffähig zu machen.

Dann eben PostgreSQL in Memory, oder?

Manche Datenbanksysteme bieten die Möglichkeit an, sie In-Memory zu starten, also ohne physische Installation. Das kann in manchen Fällen eine Alternative sein, bringt aber – spätestens über Betriebssystemsgrenzen hinweg – auch seine Probleme mit sich. Zur Erinnerung: der Vorteil von H2 war, dass alle benötigten Dateien mit der Maven Dependency angezogen wurden. Die Datenbank ist komplett in Java implementiert und damit maximal plattformunabhängig.

Wir stehen also zwischen den Möglichkeiten einer lokalen Installation (wollen wir nicht) und dem Betrieb von H2 (oder einer vergleichbaren Technologie) im Speicher betrieben (funktioniert aber leider nicht). Irgendwas muss es doch geben, was diese Lücke schließt, oder?

Unit-Tests gegen Docker Container

Für Java ist die Java Virtual Machine (JVM) der Türöffner auf alle Betriebssysteme. Etwas ähnliches ist Docker gelungen, um „Container“ im weiteren Sinne einfach verfügbar zu machen. Die für uns interessanten Container basieren auf Linux Betriebssystemen mit einer gewissen Grundinstallation und Konfiguration. Um die Arbeit mit Docker Containern etwas komfortabler zu gestalten bietet sich Docker Compose an.

In der Praxis könnte das in unserem Beispiel so aussehen: Im Repository hinterlegen wir eine Konfigurationsdatei für Docker Compose.

version: '3.5'
services:
    postgres:
    image: postgres:12
    restart: always
    environment:
         POSTGRES_USER: local
         POSTGRES_PASSWORD: local
         POSTGRES_DB: tasklist
    ports:
         - 5432:5432

Weil wir unsere KollegInnen mögen, hinterlegen wir natürlich in der README des Repositories noch einen kurzen Hinweis, dass eine Installation von Docker und Docker Compose benötigt wird. Und weil wir sie wirklich sehr mögen, verlinken wir gleich noch die Websites mit den Installationshinweisen für die unterschiedlichen Betriebssysteme ;-)

Gestartet wird der Container dann einfach mit dem folgenden Befehl auf der Kommandozeile:

docker-compose up

Bei entsprechender Konfiguration der Applikation läuft sie dann für den lokalen Run und/oder Unit-Tests gegen diesen Container.

Und auch unser Testdatendilemma wird radikal entschärft: Wir können den Container jederzeit herunterfahren und von Null auf neu bauen lassen. Ein händisches Neuaufsetzen der Datenbank entfällt, zumindest solange wir ein Datenbankmigrationstool nutzen.

Container aus Spring Boot starten?

So ganz flüssig fühlt sich der Prozess noch nicht an. Mal abgesehen von der lokalen Docker Installation, um die wir aber nicht herumkommen, gibt es doch immer einen Schritt, an den wir denken müssen: Vor dem lokalen Start oder dem Testlauf müssen wir daran denken, den Container zu starten.

Noch mehr: Wir brauchen auch für unsere Pipeline eine Lösung, dass der Container gestartet und korrekt verbunden werden. Noch schöner wäre natürlich, wenn der Container einfach von Spring Boot aus gestartet werden würde...

Testcontainers in Spring Boot

Die gute Nachricht: Ja, das ist möglich! Testcontainers heißt das Zauberwort! Die Grundidee ist eigentlich nur, den eben beschriebenen händischen Schritt zu automatisieren. Dann passiert folgendes: Wir konfigurieren unsere Unit-Tests so, dass der Container vor jedem Testlauf verfügbar gemacht wird. Je nach verwendetem Test-Framework wählen wir einen etwas anderen Ansatz, das Ziel bleibt aber das gleiche: Container starten, die ggf. automatisch generierten Credentials auslesen, in der Applikation setzen und dann die Tests gegen diese Instanz laufen lassen.

Testcontainers in Spring Boot Applikation einbinden

Um es vorweg zu nehmen: Es gibt nicht den einen, richtigen Weg für die Einbindung von Testcontainers in Spring Boot Applikationen. Aber ich zeige Euch natürlich gerne einen funktionierenden Ansatz zur Konfiguration eines Postgres Containers für Spring Boot Tests mit JUnit 5.

Als Dependency binden wir folgendes ein, in meinem Fall in Maven. In Gradle funktioniert es analog:

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>postgresql</artifactId>
    <version>1.15.1</version>
    <scope>test</scope>
</dependency>

Da ich Testcontainers nur für meine Unit Tests nutzen möchte, vergebe ich den Scope „test“. Grundsätzlich hätte ich auch die Möglichkeit eine Instanz für den lokalen Start der Applikation zu konfigurieren.

Konfiguration von Testcontainers in Spring Boot

Jetzt müssen wir noch ein bisschen Code hinzufügen. Für unser Beispiel können wir einfach die bestehende Klasse PostgreSQLContainer erweitern.

public class PostgresContainer extends PostgreSQLContainer<PostgresContainer> {
    private static final String IMAGE_VERSION = "postgres:12.5";
    private static PostgresContainer container;
   
    private PostgresContainer() {
         super(IMAGE_VERSION);
    }
   
    public static PostgresContainer getInstance() {
         if (container == null) {
              container = new PostgresContainer();
         }
         return container;
    }
   
    @Override
    public void start() {
         super.start();
         System.setProperty("DB_URL", container.getJdbcUrl());
         System.setProperty("DB_USERNAME", container.getUsername());
         System.setProperty("DB_PASSWORD", container.getPassword());
    }
   
    @Override
    public void stop() {
         //do nothing, JVM handles shut down
    }
}

Im Wesentlichen bauen wir hier die Brücke zwischen Java und Docker: Wird die Methode getInstance() erstmalig aufgerufen, wird unser Container mit dem Docker-Image postgres:12.5 gestartet und hochgefahren. Durch das Überschreiben der Methode start() übernehmen wir die Credentials wie Username, Passwort und die JDBC-Url des Containers und setzen sie in den System-Properties. Die Konfigurationsmechanismen von Spring Boot werden sich um den Rest kümmern ;-)

Unit-Tests gegen Testcontainers

Mehr ist zur Konfiguration unseres PostgreSQL Containers nicht zu tun! Das letzte Puzzlestück ist jetzt noch die Verbindung des Testcontainers mit den Unit-Tests. JUnit 5 bringt hier schon viel Konfiguration mit, so dass sich eine Basisklasse ganz einfach definieren lässt:

@SpringBootTest
@Testcontainers
public abstract class BaseTest {

    @Container
    public static PostgreSQLContainer postgreSQLContainer = PostgresContainer.getInstance();
}

Jede Testklasse, die von dieser Basisklasse erbt, wird als Datenbank unseren frisch gestarteten Testcontainer verwenden!

Startupzeiten von Testcontainers

Zeit ist ein hohes Gut im agilen Kontext. Daher drängt sich die Frage auf, wie es um die Startupzeiten der Testcontainers bestellt ist. Die gute Nachricht: Im Normalfall starten die Testcontainers in relativ kurzer Zeit. Lediglich beim ersten Start des Containers muss das Image heruntergeladen werden, das kann etwas Zeit in Anspruch nehmen.

In der Praxis starten bei mir die meisten Testcontainers in ungefähr 20 Sekunden. Bei der Ausführung eines einzelnen Tests fällt das natürlich mehr ins Gewicht, als wenn eine komplette Klasse mit mehreren Tests ausgeführt wird. H2 ist natürlich deutlich schneller… Aber wie oben beschrieben gibt es eben auch Fälle, in denen eine Verwendung einfach nicht möglich ist.

Testcontainers für Oracle

Das klingt bisher zu schön um wahr zu sein, oder? Leider ja, einen kleinen Dämpfer gibt es zum Schluss noch… Die Firma Oracle stellt für ihre Datenbanken mittlerweile öffentlich sogenannte Express Editions (Oracle XE) zur Verfügung. Auf GitHub finden sich diverse Repositories, über die man diese Versionen als Docker Images einbinden kann.

Leider musste ich feststellen, dass die neueste Version Oracle 18 XE mehrere Minuten zum Startup benötigt :-( Und das wirklich bei jedem Startup, auch für einen einzelnen Test. Während ich das ggf. in einer Pipeline noch verkraften könnte, scheidet das Konstrukt damit leider für den lokalen Testlauf aus. Hier wir es spannend, ob Oracle hier noch einmal mit einer deutlich schnelleren Lösung um die Ecke kommt…

Fazit

In vielen Fällen können Testcontainers mit Spring Boot zum Einsatz gebracht werden um gegen das gleiche Datenbanksystem zu testen, das dann auch in Produktion verwendet wird. Die Konfiguration ist wirklich schnell gemacht und die Einbindung ist genauso nahtlos, wie wir es von Spring Boot gewohnt sind. Projekt klonen und loslegen wie mit H2, nur dass wir eben lokal noch Docker installieren müssen.

Ich wünsche euch viel Spaß beim Ausprobieren!

Wenn Du Fragen zu Testcontainers 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!

Julius Mischok lächelt in die Kamera.

Danke für's lesen.

Julius Mischok ist Geschäftsführer der Mischok GmbH in Augsburg. Seine Kernaufgaben sind Prozessentwicklung, sowie Coaching und Schulung der Entwicklungsteams. Aktuell fokussiert sich seine Arbeit auf die Frage, wie Software schnell und mit einer maximalen Wertschöpfung produziert werden kann. Julius hat Mathematik studiert und entwickelt seit fast zwei Jahrzehnten Java. Seine Erfahrung brachte er unter anderem in Softwareprojekten für BMW, Audi, Hilti, Porsche, Allianz, Bosch, und viele mehr ein.