Datenbanktests mit DB Unit
Wie lassen sich eigentlich Datenbanken testen?
Während meiner täglichen Arbeit spielen auch Testdaten regelmäßig eine Rolle. Diese müssen geladen, verändert und wieder in ihren Urzustand versetzt werden. Das Zurücksetzen einer Datenbank kann dabei sehr zeitaufwendig, komplex und fehleranfällig sein. Auch das Testen von Datenbanktabellen und deren zahlreichen Eigenschaften ist dabei entscheidend. Einen bestimmten Zustand zu provozieren ist dabei alles andere als einfach.
Wie können Testdaten geladen werden, ohne eine seperate Infrastruktur aufzubauen?
Wie lassen sich bestimmte Zustände provozieren, die so im Echtbetrieb schwer erreichbar sind?
Diese Fragen und mehr lassen sich mit dem kostenlosen Werkzeug DB Unit beantworten.
DB Unit – Ein Unit-Test-Tool zum Testen relationaler Datenbankinteraktionen in Java
Die Installation von DB Unit kann sehr leicht mit Maven erfolgen.
Setup
<dependency>
<groupId>org.dbunit</groupId>
<artifactId>dbunit</artifactId>
<version>2.8.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>gsbase</groupId>
<artifactId>gsbase</artifactId>
<version>2.0.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<optional>true</optional>
</dependency>
In Version 2.7.1 wurde gsbase in den Dependencies entfernt und seither nicht mehr hinzugefügt. Diese ist jedoch weiterhin notwendig.
Trotz einer Verwendung von JUnit 5 sind die JUnit 4.13 Dependencies notwendig, da fehlerhafte Tests sonst nicht korrekt ausgewertet und angezeigt werden können.
DB Unit erste Schritte
Um DB Unit nutzen zu können, sind zunächst zwei Schritte notwendig.
- Testdatenbankstruktur vorbereiten
- Testdaten laden
1. Testdatenbankstruktur vorbereiten
Testdatenbanken bzw. deren Tabelleninformation werden als SQL Statement angegeben und in einer Schemadatei abgelegt. Es empfiehlt sich, diese schema.sql Datei unter test/resources in einem neuen Ordner schemas abzulegen.
Ein Schema kann beispielsweise wie folgt aussehen:
CREATE TABLE IF NOT EXISTS SUPERHELDEN
(
`ID` int AUTO_INCREMENT NOT NULL,
`VORNAME` varchar(100) NOT NULL,
`NACHNAME` varchar(100) NOT NULL,
`ALIAS` varchar(100) NOT NULL,
`COMICVERLAG` int NOT NULL,
PRIMARY KEY (`ID`)
);
CREATE TABLE IF NOT EXISTS COMICVERLAG
(
`ID` int AUTO_INCREMENT NOT NULL,
`NAME` varchar(100) NOT NULL,
PRIMARY KEY (`ID`)
);
Bei dieser Struktur wird eine einfache Tabelle zum Ablegen von Comic Charakteren erzeugt. In einer zweiten Tabelle befinden sich die Verläge. Über das Feld comicverlag sind die IDs verknüpft.
2. Testdaten laden
Um nun die benötigten Testdaten für die jeweilige Tabelle zu laden, wird eine XML Datei benötigt. Innerhalb dieser XML erfolgt eine Zuweisung der Feldinhalte für die jeweilige Tabelle. Begonnen wird ein Eintrag jeweils mit dem Tabellennamen, gefolgt von den jeweiligen Feldern und deren Inhalte.
Für die Comiczuordnung kann dies wie folgt außen:
<?xml version="1.0" encoding="UTF-8"?>
<dataset>
<COMICVERLAG id='1' NAME='Marvel'/>
<COMICVERLAG id='2' NAME='DC'/>
<SUPERHELDEN id='1' VORNAME='Tony' NACHNAME='Stark' ALIAS='Iron Man' COMICVERLAG='1'/>
<SUPERHELDEN id='2' VORNAME='Steve' NACHNAME='Rodgers' ALIAS='Captain America' COMICVERLAG='1'/>
<SUPERHELDEN id='3' VORNAME='Clark' NACHNAME='Kent' ALIAS='Superman' COMICVERLAG='2'/>
</dataset>
Abgelegt wird diese data.xml im Ordner data unter test/resources. Nachdem nun alle Vorbereitungen getroffen sind, können die ersten Tests in DB Unit geschrieben werden.
Erste Tests mit DB Unit
Dieser Abschnitt wird die Handhabung und erste Tests mit DB Unit näher erläutern.
Um DB Unit verwenden zu können, muss die Testklasse zuerst von DataSourceBasedDBTestCase erben.
public class DbUnitTest extends DataSourceBasedDBTestCase {
}
Direkt danach kann mit der Initialisierung begonnen werden.
DB Unit Initialisieren und beenden
In der Superclass befindet sich bereits eine setUp() Methode, die nur noch aufgerufen werden muss. Um dies zu tun, nutze ich hier eine @BeforeEach Annotation und baue außerdem eine Datenbankverbindung auf.
private Connection connection;
@Override
@BeforeEach
protected void setUp() throws Exception {
super.setUp();
connection = getConnection().getConnection();
}
@Override
@AfterEach
protected void tearDown() throws Exception {
super.tearDown();
}
Außerdem führe ich in einer @AfterEach annotierten Methode einen tearDown nach jedem Testfall durch. Die jeweiligen Methoden existieren bereits in der Superclass.
@Override
protected DatabaseOperation getSetUpOperation() {
return DatabaseOperation.REFRESH;
}
@Override
protected DatabaseOperation getTearDownOperation() {
return DatabaseOperation.DELETE_ALL;
}
Abschließend sind noch die entsprechenden Vorgehen beim Starten bzw. beenden anzugeben. Hier wird jeweils für jede Operation die Datenbasis aktualisiert und nach jedem Test der aktuelle Datenbestand aus dem Cache geleert. Somit sind die Tests unabhängig von vorherigen Testläufen.
Nach diesem kurzen Setup kann der erste Test begonnen werden.
Der erste DB Unit Test
Zunächst muss für den ersten Test noch die Datenbankverbindung allgemein geschaffen werden. Ich nutze hier eine H2 Datenbank, es wären aber auch andere möglich.
@Override
protected DataSource getDataSource() {
JdbcDataSource jdbcDataSource = new JdbcDataSource();
jdbcDataSource.setURL("jdbc:h2:mem:default;DB_CLOSE_DELAY=-1;init=runscript from 'classpath:schemas/schema.sql'");
jdbcDataSource.setUser("sa");
return jdbcDataSource;
}
@Override
protected IDataSet getDataSet() throws Exception {
try (InputStream resourceAsStream = getClass().getClassLoader().getResourceAsStream("data/data.xml")) {
return new FlatXmlDataSetBuilder().build(resourceAsStream);
}
}
In der Methode getDataSource() wird die Datenbankverbindung zur H2 Inmemory Testdatenbank aufgebaut. Dabei wird auch die zuvor verwendete schema.sql Datei geladen. Für andere Datenbanktypen ist hier eine entsprechende Anpassung notwendig.
Die Methode getDataSet() läd die in data.xml vorgegebenen Testdatensätze in die Tabellen aus getDataSource(). Nun kanns mit dem ersten Test losgehen.
@Test
public void selectHeroFromDatabase_returnCorrectResult() throws SQLException {
ResultSet rs = connection.createStatement().executeQuery("select * from SUPERHELDEN where id = 1");
Assertions.assertTrue(rs.next());
Assertions.assertEquals("Iron Man", rs.getString("ALIAS"));
}
In diesem Testfall wird einfach nur die zuvor angelegte Verbindung geprüft und ob die Daten aus data.xml korrekt ausgelesen werden.
Nachdem dies erfolgreich war, können nun weitere Tests folgen.
Weiterer Test
Im nächsten Testfall soll ein insert in den Datenbestand geprüft werden. Hierzu wird eine expected-hero.xml Datei im resources/data Ordner hinzugefügt.
Diese sieht wie folgt aus:
<?xml version="1.0" encoding="UTF-8"?>
<dataset>
<SUPERHELDEN id='4' VORNAME='Wade' NACHNAME='Wilson' ALIAS='Deadpool' COMICVERLAG='1'/>
</dataset>
Mit dem nächsten Testfall soll dieser Eintrag mit der id 4 in den Datenbestand ergänzt und anschließend geprüft werden.
import static org.dbunit.Assertion.assertEqualsIgnoreCols;
@Test
public void insertNewDataset_tableHasOneNewHero() throws Exception {
try (InputStream is = getClass().getClassLoader().getResourceAsStream("data/expected-hero.xml")) {
IDataSet expectedDataSet = new FlatXmlDataSetBuilder().build(is);
ITable expectedTable = expectedDataSet.getTable("SUPERHELDEN");
connection.createStatement().executeUpdate(
"INSERT INTO SUPERHELDEN (ID,VORNAME, NACHNAME,ALIAS,COMICVERLAG) VALUES (4, 'Wade', 'Wilson', 'Deadpool', '1')");
ITable actualData = getConnection().createQueryTable("result",
"SELECT * FROM SUPERHELDEN WHERE ALIAS = 'Deadpool'");
assertEqualsIgnoreCols(expectedTable, actualData, new String[] { "id" });
}
}
Zu Beginn wird der Inhalt von expected-hero.xml ergänzt und steht anschließend in der Tabelle zur Verfügung. Direkt danach lassen sich entsprechende Asserts darauf ausführen. assertEqualsIgnoreCols ist eine eigene Annotation von DB Unit und ermöglicht es Vergleiche durchzuführen, aber bestimmte Spalten dabei zu ignorieren. Die zurückgelieferte Tabelle muss also dem entsprechen, was wir erwarten, bis auf die ID. Es bestehen noch weitere eigene Asserts von DB Unit.
DB Unit Asserts
Mit DB Unit können die unterschiedlichsten Datenbank und Tabellenzustände provoziert werden. Um nun einen Vergleich mit einem erwarteten Zustand durchzuführen, bringt DB Unit eigene Asserts mit sich. Diese sind in der Class Assertion zu finden.
Beispielsweise ist es mit der Methode assertEquals möglich, Tabellen Zustände und deren Inhalte miteinander zu vergleichen.
Zahlreiche weitere Asserts befinden sich in besagter Class für die unterschiedlichsten Anwendungsfälle.
Fazit
DB Unit bringt zahlreiche Möglichkeiten mit sich um Testdaten und Datenbanktransaktionen zu simulieren, dies hier gezeigte ist nur ein kleiner Ausblick. Dennoch stellt sich die Frage, wie gut diese Option bei sehr großen Systemen mit mehreren Millionen Datensätzen funktioniert. Für Unittests mit Datenbanken im kleinen Rahmen ist es sehr wertvoll und unterstützt die Testdatenverwaltung für einzelne Durchläufe sehr.