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:
- 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.
- Testausführung: Die Tests werden unter Verwendung dieser containerisierten Dienste ausgeführt.
- 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:
Annotation | Beschreibung |
---|---|
@DataJpaTest | Konfiguriert eine Umgebung für JPA-Tests. |
@Testcontainers | Aktiviert 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. |
@EnableAutoConfiguration | Ermö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.
Kriterium | H2-Datenbank | Testcontainers |
---|---|---|
Kompatibilität | Eingeschränkte Feature-Unterstützung und SQL-Dialekte, nicht alle Produktionsdatenbank-Features verfügbar | Unterstützt echte Datenbank-Instanzen, hohe Kompatibilität zur Produktionsumgebung |
Realitätsnähe | Niedrig, da es sich um eine In-Memory-Datenbank handelt | Hoch, da echte Datenbanken verwendet werden |
Flexibilität | Begrenzte Flexibilität, da es sich um eine In-Memory-Datenbank handelt | Hohe Flexibilität durch Unterstützung verschiedener Datenbankversionen und -konfigurationen |
Isolierung | Keine Isolierung, da alle Tests die gleiche Datenbank verwenden | Hohe Isolierung, da jeder Test in einem separaten Container ausgeführt wird |
Performance | Sehr schnell, da es sich um eine In-Memory-Datenbank handelt | Kann langsamer sein, da Container hochgefahren werden müssen |
Ressourcenverbrauch | Niedrig, da es sich um eine In-Memory-Datenbank handelt | Höherer Ressourcenverbrauch, da Docker-Container mehr Ressourcen benötigen |
Konfiguration | Leicht, keine zusätzliche Software-Installation erforderlich | Erfordert eine funktionierende Docker-Umgebung und Docker-Kenntnisse, was die Einrichtung komplexer macht |
Anwendungsfälle | Ideal für schnelle Unit-Tests und einfache Szenarien | Perfekt für Integrationstests und Szenarien, die eine hohe Übereinstimmung mit der Produktionsumgebung erfordern |
Integration in CI/CD | Minimaler Overhead, keine Abhängigkeiten von externen Systemen | Gut in CI/CD-Pipelines integrierbar, Tests in produktionsnahen Umgebungen |
Skalierbarkeit | Nicht geeignet für Tests mit echter Datenbanklast | Geeignet für Tests, die echte Datenbanklast simulieren müssen |
Zuverlässigkeit | Geringere Aussagekraft und Zuverlässigkeit der Tests, Unterschiede zur Produktionsumgebung | Hohe Zuverlässigkeit, da Tests mit echten Datenbank-Instanzen durchgeführt werden |