Spring Boot Tests mit H2

Ein Hoch auf Microservices! Ein Hoch auf Self-Contained-Systems! Ist es nicht eine unglaubliche Erleichterung für ein Projekt, einzelne Aspekte des ganzen Systems betrachten und bearbeiten zu können? Gut, dass es diese Methoden mittlerweile gibt! Beim Gedanke an den „guten alten Monolithen“ läuft es mir immer kälter den Rücken hinunter ;-)

Durch automatisierte Tests kann ich die einzelnen Komponenten schnell und sicher implementieren, erweitern und auch das Deployment stellt keine große Gefahr mehr dar. Gerade in der Entwicklung zeigen sich die Vorteile der isolierten Bearbeitung: Im besten Fall kann ich Services ohne weitere Umgebungskonfiguration auf der Entwicklermaschine aufsetzen und testgetrieben entwickeln.

Wenn meine Services oder Systeme als Datenquelle eine relationale Datenbank nutzen, stehen mir verschiedene Möglichkeiten offen, für lokal und in Pipelines ausgeführte Unittests Datenquellen einzubinden.

Ich habe sehr gute Erfahrungen mit dem relationalen Datenbank Managementsystem H2 gemacht. H2 ist vor allem für Tests als In-Memory Lösung nutzbar, bietet sich aber auch für Prototyping und kleine Projekte an.

Was ist H2 genau?

Kurz gesagt: H2 ist eine komplett in Java implementierte Open Source SQL Datenbank. Ich kann H2 in drei verschiedenen Modi verwenden: Embedded, Server oder wie schon gesagt In-Memory. Wenn ich die Datenbank im Embedded Modus verwende, läuft sie in der gleichen JVM wie meine Applikation und ist auch nur innerhalb dieser Virtual Machine erreichbar.

Wenn ich H2 auch von anderen JVMs aus ansprechen will, benutze ich die Datenbank im ServerModus. Der In-Memory Modus funktioniert im Grunde genommen wie der Embedded Modus, nur dass der Zustand der Datenbank nicht über den Neustart der JVM persistiert wird.

Mit der H2 Datenbank sind Triggers, Views, Checkconstraints sowie Transaktionen und viele weitere Basiskonstrukte möglich. Zusätzlich kann ich bei der Verbindung Kompatibilitätsmodi für gängige SQL Datenbanken wie MySQL, SQL Server, PostgreSQL und viele mehr konfigurieren. H2 persistiert die Daten in Files, die durch AES-128 verschlüsselt werden, wenn sie nicht im In-Memory Modus läuft. Die Anbindung mit bewährten Standards wie etwa JPA ist ohne weiteres möglich, da die Datenbank JDBC unterstützt.

Auf der Projektwebsite www.h2database.com findet Ihr einen Überblick über die Features und einen übersichtlichen Vergleich zu anderen Datenbanken.

kevin ku w7ZyuGYNpRQ unsplash

Was mich an H2 begeistert

Ich finde es wirklich großartig, dass ich auf der Maschine keine manuelle Installation des Datenbanksystems vornehmen oder aber einen Container mit einer Installation hochfahren muss. Mir persönlich hat das die Arbeit in diversen Projekten wirklich sehr erleichtert! H2 ist komplett in Java geschrieben und so kann ich die Datenbank per Dependency in meinem jeweiligen Build-Tool konfigurieren und nach dem Klonen des Projekts auf der Entwicklermaschine automatisch anziehen lassen – Einfacher geht es kaum noch!

Im In-Memory Modus kann ich bei jedem Testlauf (oder eben JVM Start) mit einem definierten Datenbankzustand starten. Automatisierte Tests kann ich so sehr viel einfacher reproduzieren.

H2 fügt sich durch die reine Integration als Dependency nahtlos in CI/CD Pipelines ein, ohne dass ich eine angepasste Konfiguration vornehmen muss. Nicht zuletzt hier zeigen sich die Vorteile eines sehr schmalen Footprints in Laufzeit und Ressourcenverbrauch!

Wie Konfiguriere ich H2 nun für den In-Memory Modus?

Unter https://gitlab.mischok-it.de/open/tasklist seht Ihr ein komplettes Beispiel für die Konfiguration der H2 Datenbank in einer Spring Boot Applikation. Der Service bietet ein relativ einfaches REST Interface zur Verwaltung von Tasks. Im Produktivbetrieb möchte ich eine PostgreSQL Datenbank nutzen und habe diese auch schon unter src/main/resources/application.properties konfiguriert.

spring.datasource.username=local
spring.datasource.password=local
spring.datasource.url=jdbc:postgresql://localhost:5432/postgres
spring.datasource.driverClassName=org.postgresql.Driver

Im Repository habe ich eine Docker Compose Konfiguration, die einen Container mit einer Datenbankinstanz hochfährt. Mit dieser lasse ich die Applikation lokal laufen.

Ich muss zwei Steps unternehmen um die Unittests nicht mehr gegen die PostgreSQL Datenbank laufen zu lassen. Erst binde ich die Dependency in der Maven-Konfiguration ein. Analog kann ich natürlich auch Gradle konfigurieren.

...
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>

Danach erweitere ich eine bereits bestehende src/test/resources/application.properties oder lege die Datei an. Grundsätzlich muss ich hier mindestens vier Parameter überschreiben

  • Die URL meiner H2 Datenbank
  • Benutzername
  • Passwort
  • Der zu verwendende Treiber
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password

Der Treiber wird bereits angezogen, da die Dependency im Buildfile hinzugefügt ist. Durch den Einschub „:mem“ in der Datenbank-URL wird die Datenbank im In-Memory Modus betrieben.

Im besten Fall war´s das! So kann ich die Unittests schon gegen die H2 Datenbank ausführen!

Wie oft haben wir aber das Problem, dass wir ein Bestandsprojekt übernehmen, das lokal noch überhaupt nicht testbar ist? Ich kann mich da an zahllose wilde Konfigurationen außerhalb des Projekts erinnern, die selten wirklich Spaß machen. Mit dem oben gezeigten Weg kann ich mir hier einigen Ärger ersparen! Trotzdem kann es bei der Umsetzung immer noch zu einigen bösen Überraschungen kommen – je nach Zustand des Bestandscodes.

Zum Beispiel, wenn keine Datenbank-Migrationsskripte zur Schemagenerierung benutzt wurden. In diesem Fall muss ich Tabellen und weitere Strukturen für die Testläufe auf anderem Weg anlegen. Am besten prüfe ich zunächst, ob ich die Tabellenstruktur anhand von JPA Entities anlegen kann. Dazu kann ich den Konfigurationsparameter spring.jpa.hibernate.ddl-auto=create-drop in die Testkonfiguration aufnehmen.

Wenn das nicht klappt, kann ich für die Testläufe ein Datenbankmigrationstool einbinden (ich kann zum Beispiel Flyway empfehlen: https://flywaydb.org/). Dafür lege ich dann ein einzelnes Migrationsskript an und lasse mir dann die gewünschte Tabellenstruktur in H2 erstellen.

Vielleicht ist aber für den Produktivbetrieb schon ein Datenbankmigrationstool konfiguriert. Dann kann es mir natürlich passieren, dass die Migrationsskripte teilweise zu H2 inkompatibel sind. Möglicherweise kann mir in diesem Falle der Kompatibilitätsmodus der H2 Datenbank helfen. Etwa wenn bislang herstellerspezifischer Dialekt oder Strukturen in den Skripten genutzt wurden.

Falls auch der Kompatibilitätsmodus nicht greift, nutze ich meine letzte Möglichkeit und überschreibe einzelne Skripte in den Test-Resources, übersetze diese also in Standard SQL, soweit nötig.

In fast allen Fällen sollte ich mit diesem Vorgehen ein Setup herstellen, mit dem ich in der Lage bin, sinnvolle Unittests zu erstellen. Diese kann ich lokal und in Pipelines ausführen und als wertvollen Baustein zur Qualitätssicherung nutzen.

Meine Erfahrungen aus der praktischen Anwendung zeigen, dass die Aufwände für die Konfiguration am Ende gut mit dem erreichten Nutzen in Relation zu setzen sind.

simon abrams k T9Zj3SE8k unsplash

Kritische Betrachtung

Wir haben also jetzt die technische Konfiguration geklärt! Was jetzt auf dem Programm steht ist natürlich die Betrachtung des Ganzen aus Sicht einer nachhaltigen Softwarearchitektur. Vor allem müssen wir uns kritisch fragen, ob es vertretbar ist, für die automatisierten Tests und in Produktion unterschiedliche Datenbanksysteme zu verwenden.

Generell steht hier natürlich ein klares Nein! Nach agiler Lehre will ich ja die Zeit minimieren, nach der Fehler auftreten. Wenn ich verschiedene Datenbanksysteme nutze, kann es mir passieren, dass ein spezifischer Effekt beim lokalen Testlauf noch nicht auftritt, wohl aber nach der Integration. Insgesamt müssen wir aber auch die Zeitersparnis sehen, da eine lokale Konfiguration sowie die Konfiguration in den Pipelines wegfällt.

Dazu kommt: Wenn ich mich in einer Microservice-Architektur bewege, kann sich meine Entscheidung durchaus positiv auf das Softwaredesign auswirken. Die Verwendung zweier Datenbanksysteme parallel zwingt mich in der Entwicklung zur Verwendung von Standards. 

Mein Lieblingsbeispiel ist an dieser Stelle immer die Verwendung von SQL in Migrationsskripten. Langfristig ist der Microservice nur schwach an mein Datenbanksystem gekoppelt und ich kann im Produktivbetrieb relativ problemlos auf ein anderes System umziehen wenn es nötig wird. Außerdem hat der Service einen größeren Fokus auf die nach außen zur Verfügung gestellte Schnittstelle, wenn die Funktion nicht an die technischen Möglichkeiten der Datenbank im Hintergrund gekoppelt ist.

Meine Faustregel ist mittlerweile: Je kleiner das Projekt und je wichtiger die Struktur der Daten gegenüber der Abfrageperformance, desto eher habe ich mehr Vor- als Nachteile durch den Einsatz von verschiedenen Datenbanksystemen für automatisierte Tests und Produktivbetrieb.

Im Endeffekt ist es aber wie immer: Pauschale Aussagen sind problematisch. Eine endgültige Bewertung und Entscheidung kann immer nur individuell auf die jeweilige Applikation abgestimmt werden.

Die Grenzen von H2

Als die NoSQL Datenbanken aufkamen, haben sie den Markt für Datenbanksysteme komplett verändert. Nach wie vor punkten relationale Datenbanksysteme aber bei der Konsistenz, Transaktionssicherheit und Abfrageperformance, vor allem dann, wenn ich in der Datenbank selbst schon viel Optimierungspotential ausschöpfen kann. 

Ist die Performance eine wichtige fachliche Anforderung an meine Applikation, sollte ich die Möglichkeiten des dahinterliegenden Systems möglichst ausreizen. In diesem Fall rate ich von H2 als Datenbank ausdrücklich ab, da sonst für einen Teil der Anforderungen nicht testgetrieben entwickelt werden kann. Das gleiche gilt, wenn ich aufgrund von fachlichen Anforderungen herstellerspezifischer Strukturen des verwendeten Datenbanksystems verwenden muss.

Wenn ich nun aus den genannten Gründen H2 lieber nicht benutze, sind Testcontainer meine erste Alternative. Damit kann ich ohne externe Konfiguration Container mit einer spezifischen Datenbank (etwa PostgreSQL) aus dem Java-Code heraus erstellen und als Datenquelle einbinden. Hier ist aber etwas mehr Aufwand nötig als bei der Konfiguration von H2, vor allem werden für das Aufsetzen des Testcontainers Javaklassen erstellt. Das ist insgesamt noch im Rahmen, führt aber auf jeden Fall zu höherer Komplexität im Projekt und natürlich zu einer größeren Codebase.

Ansonsten kann ich Euch noch empfehlen, einen lokal zu startenden Docker-Container zu nutzen. Dazu checke ich ein Dockerfile oder eine Docker-Compose Konfiguration in das Repository ein. Hier bleibt die Konfiguration fast genauso schlank wie bei der Verwendung von H2. Allerdings werden auf der Entwicklermaschine zusätzlich installierte Komponenten vorausgesetzt und die Datenbank wird nicht ohne weiteres für jeden Testlauf auf einen leeren Zustand zurückgesetzt. Außerdem fallen auf jeder Entwicklermaschine und in jeder Pipeline weitere Konfigurationsschritte an.

Weitere Einsatzmöglichkeiten

H2 bietet sich aber nicht nur für den Einsatz bei automatisierten Tests an, den ich Euch vorgestellt habe. Auch beim Prototyping kommt der schlanke Footprint von H2 voll zum tragen. Ich kann in der ersten Phase einer Prototyp-Entwicklung komplett auf die Konfiguration einer externen Datenbanklösung verzichten. An sich eine kleine Zeitersparnis, die sich aber über die Anzahl der lokalen Entwicklungsumgebungen und Deployment-Stages ziemlich stark multipliziert. Der Wechsel auf ein anderes Datenbanksystem ist dann durch die Verwendung von standardkonformem SQL und Abstraktions-Layern wie JPA kein Problem.

Ich empfehle H2 aber auch immer dann, wenn eine schlanke Installation des gesamten Systems nötig ist. Mein Lieblingsbeispiel sind kleine Services, die von wenigen Nutzern verwendet werden. Da eine separate Datenbankinstallation mit den verbundenen Betriebsaufgaben (Updates, Backups, usw.) wegfällt, sparen wir uns über die gesamte Lebensdauer der Applikation einiges an Aufwänden. Die H2 Datenbank hat sogar ein kleines Webinterface mit dabei, das ich im Produktivbetrieb in einer Spring Boot Applikation mit spring.h2.console.enabled=true aktivieren kann.

Fazit

Was mich am relationalen Datenbanksystem H2 begeistert, ist die schlanke Konfiguration und der schmale Footprint. Das hat mir bei Prototypen oder als Datenbank für Unittests schon viel Arbeit erspart. Auch in Bestandsprojekten, in denen keine automatisierten Tests durchgeführt wurden kann ich das System nachrüsten. Auf diese Art erreiche ich in meinen Applikationen eine nachhaltige Qualitätssteigerung. Aufgrund der Verwendung von Standard SQL und Abstraktionsschichten in der Anbindung ist die Applikation nur lose an das verwendete Datenbanksystem gekoppelt und ich erreiche ein insgesamt fundamental verbessertes Applikations-Design.

Spring-Boot News

Bleibe auf dem aktuellsten Stand mit unseren kostenlosen Spring-Boot updates. So wirst Du direkt informiert, wenn wir einen neuen Artikel veröffentlichen.
Kein Spam, kein Bullshit, nur Spring-Boot Insider-Wissen, versprochen.

Noch keine Kommentare vorhanden.

Was denkst du?

Grundlagenkurs Java für Einsteiger

Der beste Weg Java zu lernen? Mit echten Profis.

Der zertifizierte Praxiskurs der Mischok Academy. Remote // 3 Wochen Laufzeit // nur 1.450 Euro // Kursstart im September // Mehr erfahren👇
Mehr Infos zum Java-Kurs
© 2020 Mischok