Datenbanktests mit Testcontainern

Foto des Autors
Ahmad Aljeld Zubaydi

Entdecke in unserem neuen Blogbeitrag, wie du mit Testcontainers und Docker eine realistische und isolierte Testumgebung für Datenbanken schaffst. Erfahre, wie Testcontainers deine Integrationstests optimiert und vergleiche sie mit H2-Datenbanktests.

Titelbild zum Wissensbeitrag "Datenbanktests mit Testcontainern"

In einem früheren Blogbeitrag haben wir das Testen von Datenbanken behandelt und einen Testansatz mit der H2-Datenbank vorgestellt.

Nun möchte ich einen weiteren Ansatz vorstellen, der sich als sehr effizient für das Testen von Datenbanken erwiesen hat. Ich werde ein kleines Beispiel zeigen, wie diese Methodik in Java umgesetzt werden kann.

Anschließend folgt ein Vergleich zwischen dem Testen mit H2 und dem Testen mit Testcontainer.

Test-Container

Testcontainers ist eine Testbibliothek, die die Nutzung von Docker-Containern beim Testen, insbesondere in Integrationstests, ermöglicht. Dabei werden Docker-Container verwendet, um die benötigten Dienste wie Datenbanken, Messaging-Systeme und andere in einer isolierten Umgebung zu starten.

Testcontainers kümmert sich um den gesamten Lebenszyklus der Container, von der Erstellung über die Konfiguration bis hin zur Zerstörung nach den Tests. Dies stellt sicher, dass die Tests konsistent und reproduzierbar sind, unabhängig von der Entwicklungsumgebung oder dem CI/CD-System.

Dies erlaubt es Entwicklern, ihre Tests mit der gleichen Art von Diensten durchzuführen, die sie in der Produktion verwenden, ohne Mocks oder In-Memory-Dienste zu benötigen und ohne die Testumgebung manuell einrichten zu müssen.

Ein typischer, auf Testcontainers basierender Integrationstest durchläuft drei Phasen:

  1. Testvorbereitung: In dieser Phase werden die benötigten Dienste (Datenbanken, Messaging-Systeme usw.) mithilfe der Testcontainers-API und Docker-Containern basierend auf den Anwendungskonfigurationen gestartet.
  2. Testausführung: Die Tests werden unter Verwendung dieser containerisierten Dienste ausgeführt.
  3. Aufräumphase: Testcontainers sorgt dafür, dass die Container wieder entfernt werden, unabhängig davon, ob die Tests erfolgreich waren oder Fehler aufgetreten sind.

Diese Struktur stellt sicher, dass die Tests isoliert und reproduzierbar sind und dass alle genutzten Ressourcen am Ende des Testlaufs sauber entfernt werden.

Als Nächstes werden wir den Schwerpunkt auf die Verwendung von Testcontainers im Kontext von Datenbanktests legen. Dabei werden wir die Vor- und Nachteile von Testcontainers bei Datenbanktests erläutern und ein kleines Demobeispiel präsentieren, das zeigt, wie die Datenbank-Integrationstests mit Testcontainer konfiguriert und implementiert werden können.

Welche Vorteile bringt der Einsatz von Testcontainers bei Datenbanktests mit sich?

  • Ressourcenverwaltung:
    Testcontainers bieten eine effiziente Ressourcenverwaltung, indem sie Container während der Tests nur temporär ausführen, um Ressourcenverschwendung zu vermeiden. Außerdem reinigen sie automatisch die Container nach den Tests und gewährleisten so einen sauberen Zustand für jeden Testlauf, ohne manuelle Eingriffe.
  • Einfache Konfiguration:
    Testcontainers bieten eine einfache Möglichkeit, verschiedene Arten von Datenbanken zu verwenden, ohne komplexe Konfigurationen durchführen zu müssen. Dazu können verschiedene Datenbanken wie MySQL, PostgreSQL, MongoDB usw. schnell in Docker-Containern mit der benötigten Version gestartet und verwendet werden.
  • Unterstützung verschiedener Programmiersprachen:
    Testcontainers können mit vielen gängigen Programmiersprachen, darunter Java, .NET, Go, NodeJS, Rust und Python eingesetzt werden.
  • Automatisierung:
    Testcontainers erleichtern die Automatisierung von Tests, da die Einrichtung und Bereinigung der Testumgebung automatisch erfolgt. Der Docker-Container kann in Test-Skripten gestartet und gestoppt werden, ohne dass manuell eingegriffen werden muss.
  • Nahtlose Integration:
    Testcontainers ermöglichen eine nahtlose Integration in gängige Test-Frameworks und -Tools, was eine einfache Einbindung in bestehende Projekte gewährleistet und sicherstellt, dass die getestete Umgebung der Produktionsumgebung entspricht. Durch die Verwendung von Testcontainern werden Tests mit realen Datenbanken und anderen Diensten ohne die Notwendigkeit von Mocks oder Stubs durchgeführt, was zu schnelleren, zuverlässigeren Tests und verbesserter Testisolation führt.
  • Effiziente Testumgebung:
    Testcontainers bieten eine integrierte Lösung für effiziente Testumgebungen, indem sie infrastrukturlose Integrationstests ermöglichen, einfache Bereitstellung isolierter Umgebungen, datenkonfliktfreie Build-Pipelines durch Isolation gewährleisten und portablen sowie konsistenten Testbetrieb ermöglichen.

Was sind potenzielle Nachteile, die beim Verwenden von Testcontainern für Datenbanktests auftreten können?

Die Verwendung von Testcontainern für Datenbanktests kann einige potenzielle Nachteile mit sich bringen:

  • Performance:
    Das Hochfahren von Containern kann zeitaufwendig sein, besonders bei mehreren gleichzeitigen Tests oder auf ressourcenbegrenzten Entwicklungsrechnern. Dieses Problem tritt verstärkt bei Tests auf, die jedes Mal einen Neustart des Containers erfordern, da wiederholtes Starten und Stoppen die Ausführungszeit verlängert, besonders bei komplexen oder datenintensiven Tests.
  • Komplexität:
    Die Konfiguration und Einrichtung von Testcontainern können für Entwickler zusätzliche Komplexität bedeuten, insbesondere wenn sie nicht mit Docker oder anderen Container-Technologien vertraut sind.
  • Abhängigkeit von Docker:
    Die Verwendung von Testcontainern setzt voraus, dass Docker auf dem Entwicklungssystem installiert ist. Dies kann zu Abhängigkeiten und Kompatibilitätsproblemen führen, insbesondere wenn Docker nicht Teil des Standard-Entwicklungstools ist. Des Weiteren könnten Testfälle scheitern, wenn sie das Docker-Image nicht herunterladen können.
  • Ressourcenverbrauch:
    Das Ausführen von Datenbanken in Containern kann zusätzliche Ressourcen wie CPU, Speicher und Netzwerkbandbreite verbrauchen, insbesondere wenn mehrere Tests gleichzeitig ausgeführt werden, was zu Leistungseinbußen auf dem Hostsystem führen kann.
  • Wartungsaufwand:
    Die Wartung der Testcontainer-Infrastruktur erfordert regelmäßige Updates und Überwachung, um sicherzustellen, dass alle Container und Datenbankinstanzen ordnungsgemäß funktionieren.
  • Debugging: Das Debuggen von Problemen in Testcontainern kann schwierig sein, da die Umgebung isoliert ist und zusätzliche Tools erforderlich sein können.

Diese Nachteile kann man jedoch bereitwillig in Kauf nehmen, wenn man sie den zahlreichen Vorteilen gegenüberstellt.

Einrichtung der Testcontainer in einem Spring Boot-Projekt

Im Folgenden wird gezeigt, wie die Testcontainer zwecks Datenbanktests mit PostgreSQL in ein Spring Boot-Projekt integriert werden können.

1. Hinzufügen der benötigen Abhängigkeiten:

Genau wie bei dem Beispiel von H2-Datenbank brauchen wir die folgenden Abhängigkeiten.

<dependencies>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-data-jpa</artifactId>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-web</artifactId>
	</dependency>
	<dependency>
		<groupId>org.projectlombok</groupId>
		<artifactId>lombok</artifactId>
		<optional>true</optional>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-test</artifactId>
		<scope>test</scope>
	</dependency>
</dependencies>
Code-Sprache: Java (java)

Zusätzlich werden die folgenden Abhängigkeiten für die Integration und das Testen einer PostgreSQL-Datenbank in einem Spring Boot-Projekt benötigt:

PostgreSQL JBCD Driver:
Diese Abhängigkeit stellt den JDBC-Treiber für PostgreSQL bereit, der zur Laufzeit benötigt wird, um eine Verbindung zur PostgreSQL-Datenbank herzustellen.

dependency>
  <groupId>org.postgresql</groupId>
  <artifactId>postgresql</artifactId>
  <scope>runtime</scope>
</dependency>
Code-Sprache: Java (java)

Testcontainers JUnit Jupiter:
Diese Abhängigkeit ermöglicht die Integration von Testcontainers mit JUnit 5, was die Ausführung von Integrationstests in einer Container-Umgebung unterstützt.

<dependency>
  <groupId>org.testcontainers</groupId>
  <artifactId>junit-jupiter</artifactId>
  <version>1.17.6</version>
  <scope>test</scope>
</dependency>
Code-Sprache: Java (java)

Testcontainers PostgreSQL Modul:
Diese Abhängigkeit stellt spezielle Unterstützung für die Verwendung von PostgreSQL in Testcontainers bereit. Sie wird für das Testen von Anwendungen verwendet, die PostgreSQL als Datenbank verwenden, indem sie eine PostgreSQL-Datenbank in einem Docker-Container für Integrationstests bereitstellt.

<dependency>
  <groupId>org.testcontainers</groupId>
  <artifactId>postgresql</artifactId>
  <version>1.17.6</version>
  <scope>test</scope>
</dependency>
Code-Sprache: Java (java)
2. Erstellen einer Entitätsklasse und das Repository:

Die Java-Klassen aus dem H2-Datenbank-Beispiel können unverändert übernommen werden.

3. Aufsetzen der Testklasse

Im Folgenden wird demonstriert, wie man Testcontainers verwendet, um eine PostgreSQL-Datenbank für Integrationstests in einem Spring Boot-Projekt zu nutzen. Eine detaillierte Erläuterung ist unten dargestellt:
Als erstes werden die folgenden Annotationen für Testkonfiguration benötigt:

AnnotationBeschreibung
@DataJpaTestKonfiguriert eine Umgebung für JPA-Tests.
@TestcontainersAktiviert die Integration mit Testcontainers.
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)Verhindert das automatische Ersetzen der Datenbank durch eine In-Memory-Datenbank.
@ContextConfiguration(classes = {DatabaseRepository.class})Lädt die angegebene Konfiguration für den Test.
@EnableAutoConfigurationErmöglicht die automatische Konfiguration von Spring Boot.

Danach muss ein Datenbank-Container mit den gewünschten Einstellungen, in diesem Fall eine PostgreSQL-Datenbank, initialisiert werden:

@Container
  public static PostgreSQLContainer container = new PostgreSQLContainer("postgres:11.8")
      .withDatabaseName("testContainerDb")
      .withUsername("user")
      .withPassword("password");
Code-Sprache: Java (java)

Anschließend muss nun die Verbindung zur Datenbank basierend auf den Konfigurationen des Datenbank-Containers hergestellt werden:

@DynamicPropertySource
  static void setDatasourceProperties(DynamicPropertyRegistry propertyRegistry) {
    propertyRegistry.add("spring.datasource.url", container::getJdbcUrl);
    propertyRegistry.add("spring.datasource.password", container::getPassword);
    propertyRegistry.add("spring.datasource.username", container::getUsername);
    propertyRegistry.add("spring.jpa.hibernate.ddl-auto", () -> "update");
  }
Code-Sprache: Java (java)

Nachdem wir die Umgebung für Integrationstests mit einer PostgreSQL-Datenbank unter Verwendung von Testcontainers mit den richtigen Datenbankeinstellungen konfiguriert haben, sind wir nun in der Lage, unsere Datenbank gegen eine echte Datenbankinstanz wie in der Produktion zu testen.

In der folgenden Testklasse ist ein Integrationstest für das CarRepository implementiert, der den PostgreSQL-Testcontainer konfiguriert und einen einfachen Testfall ausführt, der das Speichern, Abrufen und Löschen von Datenbank-Eintrag demonstriert.

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import java.util.Optional;

@DataJpaTest
@Testcontainers
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@ContextConfiguration(classes = {CarRepository.class})
@EnableAutoConfiguration
class CarRepositoryContainerTest {

  @Container
  public static PostgreSQLContainer container = new PostgreSQLContainer("postgres:11.8")
      .withDatabaseName("testContainerDb")
      .withUsername("user")
      .withPassword("password");

  @DynamicPropertySource
  static void setDatasourceProperties(DynamicPropertyRegistry propertyRegistry) {
    propertyRegistry.add("spring.datasource.url", container::getJdbcUrl);
    propertyRegistry.add("spring.datasource.password", container::getPassword);
    propertyRegistry.add("spring.datasource.username", container::getUsername);
    propertyRegistry.add("spring.jpa.hibernate.ddl-auto", () -> "update");
  }


  @Autowired
  private CarRepository carRepository;

  @Test
  void givenCarWhenSaveThenGet() {
    Car postgresql = new Car(1L, "BMW", "red", "auto");
    Car savedCar = carRepository.save(postgresql);

    Optional<Car> foundCar = carRepository.findById(1L);
    Assertions.assertThat(foundCar)
              .isPresent()
              .contains(savedCar);

    carRepository.deleteById(1L);

    Assertions.assertThat(carRepository.findById(1L))
              .isEmpty();
  }
}
Code-Sprache: Java (java)

Vergleich zwischen H2-Datenbank und Testcontainers

Die Entscheidung zwischen der H2-Datenbank und Testcontainers für das Testen von Datenbanken hängt von den spezifischen Bedürfnissen des Projekts ab, um di realistische Bedingungen zu simulieren und gleichzeitig die Performance und Effizienz zu gewährleisten.

Die folgende Vergleichstabelle zeigt die wichtigsten Unterschiede zwischen der H2-Datenbank und Testcontainers im Hinblick auf das Testen von Datenbanken. Beide Tools haben ihre Stärken und Schwächen, die für verschiedene Anwendungsfälle geeignet sind.

KriteriumH2-DatenbankTestcontainers
KompatibilitätEingeschränkte Feature-Unterstützung und SQL-Dialekte, nicht alle Produktionsdatenbank-Features verfügbarUnterstützt echte Datenbank-Instanzen, hohe Kompatibilität zur Produktionsumgebung
RealitätsnäheNiedrig, da es sich um eine In-Memory-Datenbank handeltHoch, da echte Datenbanken verwendet werden
FlexibilitätBegrenzte Flexibilität, da es sich um eine In-Memory-Datenbank handeltHohe Flexibilität durch Unterstützung verschiedener Datenbankversionen und -konfigurationen
IsolierungKeine Isolierung, da alle Tests die gleiche Datenbank verwendenHohe Isolierung, da jeder Test in einem separaten Container ausgeführt wird
PerformanceSehr schnell, da es sich um eine In-Memory-Datenbank handeltKann langsamer sein, da Container hochgefahren werden müssen
RessourcenverbrauchNiedrig, da es sich um eine In-Memory-Datenbank handeltHöherer Ressourcenverbrauch, da Docker-Container mehr Ressourcen benötigen
KonfigurationLeicht, keine zusätzliche Software-Installation erforderlichErfordert eine funktionierende Docker-Umgebung und Docker-Kenntnisse, was die Einrichtung komplexer macht
AnwendungsfälleIdeal für schnelle Unit-Tests und einfache SzenarienPerfekt für Integrationstests und Szenarien, die eine hohe Übereinstimmung mit der Produktionsumgebung erfordern
Integration in CI/CDMinimaler Overhead, keine Abhängigkeiten von externen SystemenGut in CI/CD-Pipelines integrierbar, Tests in produktionsnahen Umgebungen
SkalierbarkeitNicht geeignet für Tests mit echter DatenbanklastGeeignet für Tests, die echte Datenbanklast simulieren müssen
ZuverlässigkeitGeringere Aussagekraft und Zuverlässigkeit der Tests, Unterschiede zur ProduktionsumgebungHohe Zuverlässigkeit, da Tests mit echten Datenbank-Instanzen durchgeführt werden

Das könnte Dich auch noch interessieren

Continuous Deployment einer Ionic-App mit Jenkins

Continuous Deployment einer Ionic-App mit Jenkins

In Zeiten der fortschreitenden Automatisierung wird auch im Bereich der Software-Entwicklung an der Optimierung der Prozesse gearbeitet. Keine moderne Softwareschmiede ...
Wie Pair Programming variiert werden kann

Wie Pair Programming variiert werden kann

Gibt es mögliche Variationen, die es für das Team effektiver machen können? Ja, die gibt es! Und ich möchte nachfolgend ...
Titelbild zum Beitrag "Behavior Driven Development und seine Umsetzung mit Cucumber"

Behavior Driven Development und seine Umsetzung mit Cucumber

Tauchen wir gemeinsam in die Welt des Behavior Driven Development ein. Lass uns entdecken, wie diese Methode die Art und ...