Integrationstests mit Spring Data MongoDB und Testcontainers

Bis vor kurzem haben wir auf Arbeit eine Embedded MongoDB zum Testen unserer Spring Boot Anwendungen verwendet. Mittlerweile benötigen wir jedoch ein paar Features, die seit MongoDB 4 hinzugekommen sind, und die Embedded MongoDB unterstützt bis jetzt nur MongoDB 3. Daran wird sich vermutlich auch in Zukunft nichts ändern (Is this project dead? #314), sodass ich mich auf die Suche nach einer Alternative gemacht habe.

Dabei bin ich zunächst über den Artikel Integration testing with Docker and Maven von Michael Simons gestolpert und fand den Ansatz sehr interessant, beliebige Docker Container mit dem maven-failsafe-plugin in Kombination mit dem docker-maven-plugin von fabric8 während des Maven Buildprozesses zu verwenden. Die Lösung hat jedoch einen Haken: einzelne Integrationstests lassen sich nicht ohne Umwege in der lokalen Entwicklungsumgebung ausführen.

Dann bin ich jedoch auf die Testcontainers-Bibliothek gestoßen, mit der es möglich ist, beliebige Docker Container aus Java heraus zu starten. Für eine Vielzahl an Datenbanken und Infrastruktur-Komponenten stehen fertige Module zur Verfügung, die eine Integration vereinfachen. Von Haus aus werden die Testframeworks JUnit 4, JUnit 5 und Spock unterstützt.

Projekt anlegen

Zunächst wird ein Maven Projekt angelegt. In diesem Beispiel wird Spring Data MongoDB mit JUnit 5 und Testcontainers kombiniert. Die Konfiguration dazu sieht wie folgt aus:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.6.RELEASE</version>
    </parent>

    <groupId>example</groupId>
    <artifactId>example</artifactId>
    <version>0.0.0-SNAPSHOT</version>

    <properties>
        <java.version>11</java.version>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

    <dependencies>

        <!-- Spring -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-mongodb</artifactId>
        </dependency>

        <!-- Test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.testcontainers</groupId>
            <artifactId>testcontainers</artifactId>
            <version>1.14.0</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.testcontainers</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>1.14.0</version>
            <scope>test</scope>
        </dependency>

    </dependencies>

</project>

Testcontainers-Konfiguration für MongoDB

Die Anbindung von MongoDB mit Testcontainers ist so einfach, dass die Entwickler sich mit einem Beispiel begnügen, statt ein eigenständiges Modul zu entwickeln (siehe Add Support For MongoDB #1574).

Das Beispiel wurde dahingehend angepasst, dass die Verbindungsdaten zur Datenbank einfach auslesbar sind und der Container nicht automatisch nach einer Testklasse gestoppt wird. So kann der Container für viele Integrationstests nacheinander wiederverwendet werden und wird stattdessen beim Beenden der JVM automatisch gestoppt.

package example;

import org.testcontainers.containers.GenericContainer;

class MongoDBContainer extends GenericContainer<MongoDBContainer> {

    private static final int PORT = 27017;

    public MongoDBContainer(final String image) {
        super(image);
        this.addExposedPort(PORT);
    }

    public String getUri() {
        final String ip = this.getContainerIpAddress();
        final Integer port = this.getMappedPort(PORT);
        return String.format("mongodb://%s:%s/test", ip, port);
    }

    @Override
    public void stop() {
        // let the JVM handle the shutdown
    }

}

Basisklasse für alle Integrationstests

Bei der Verwendung von Spring werden die Tests für gewöhnlich am schnellsten ausgeführt, wenn derselbe Kontext für alle Tests verwendet wird. Deshalb wird die folgende Basisklasse für alle Integrationstests verwendet:

package example;

import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

@SpringBootTest
@Testcontainers
public abstract class AbstractIntegrationTest {

    @Container
    private static final MongoDBContainer MONGO_DB = new MongoDBContainer("mongo:4.2.5");

    @DynamicPropertySource
    private static void mongoDBProperties(final DynamicPropertyRegistry registry) {
        registry.add("spring.data.mongodb.uri", MONGO_DB::getUri);
    }

}

@DynamicPropertySource steht seit Spring Boot 2.2.6 zur Verfügung (siehe @DynamicPropertySource in Spring Framework 5.2.5 and Spring Boot 2.2.6) und sorgt in diesem Beispiel dafür, dass die Verbindungsdaten für die Datenbank konfiguriert werden, bevor der Spring Kontext gestartet wird.

Integrationstest schreiben

Mit dem folgendem Test kann geprüft werden, ob die Konfiguration erfolgreich ist:

package example;

import static org.assertj.core.api.Assertions.assertThat;

import java.util.UUID;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;

class MongoDBContainerTest extends AbstractIntegrationTest {

    @Autowired
    private MongoTemplate mongo;

    @Test
    void testConnection() {
        final String uuid = UUID.randomUUID().toString();
        this.mongo.createCollection(uuid);
        assertThat(this.mongo.collectionExists(uuid)).isTrue();
    }

}

Weitere Quellen