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.

  1. Testdatenbankstruktur vorbereiten
  2. 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.

Interne Schulungen in Cucumber & Selenium


In geschützter Umgebung lernt und festigt Ihr Team individuelles Wissen für Ihre aktuellen Anforderungen. Damit Ihr Projekt erfolgreich und wie geplant abgeschlossen wird.

Das könnte Sie auch interessieren

Fünf Tipps für besseren Testcode

Fünf Tipps für besseren Testcode Good Practices In diesem Blogpost zeige ich fünf Good Practices auf, um den eigenen Testcode zu verbessern. Aber wieso nur "Good...

Rolladensteuerung mit Loxone und Java

Automatische Rolladensteuerung mit Java und Loxone Motivation Loxone ist ein bekannter Anbieter für Smarthome Rolläden. Während sich über die mitgelieferte Oberfläche...

Devops mit Cucumber, Jira und Xray

Devops mit Cucumber, Jira und Xray "Hat auf meiner Maschine funktioniert, ist jetzt Operating Problem“ Diese oder vergleichbare Aussagen sind nicht neu und...

Erinnerungsassistent für Medikamente mit Sprachausgabe

Erinnerungsassistent für Medikamenteinnahme mit Sprachausgabe Motivation Bevor Corona began, besuchten Ich und meine Familie meine Großeltern regelmäßig. Dabei...

ReSpeaker 4 LEDs mit Pi4J steuern

ReSpeaker 4 LEDs mit Pi4J steuern Motivation Für ReSpeaker im allgemeinen gibt es bereits zahlreiche Bibliotheken, auch einige die LEDs über die GPIO Pins ansteuern....

Sealed Classes mit Java JDK 17

Sealed Classes mit Java JDK 17 Was sind Sealed Classes? Mit dem kommenden Java Release 17 werden sogenannte Sealed Classes eingeführt. Diese waren zuvor bereits seit...

Behaviour Driven Development (BDD) mit Cucumber und Gherkin Schulung

Behaviour Driven Development (BDD) mit Cucumber Schulung Schulungsbeschreibung In dieser Schulung lernen Teilnehmer*innen den sicheren Umgang mit BDD, Cucumber und...

Akzeptanztests mit Gauge und Java

Akzeptanztests mit Gauge und Java 1. Einführung In diesem Blogeintrag werde ich zunächst auf Gauge eingehen, die Installation vorstellen und anschließend ein Beispiel...

API Testing mit Java und WireMock

API Testing mit Java und WireMock Motivation Ein bekanntes Problem: Öffentliche APIs werden abgefragt und liefern Daten in Form von JSON oder anderen Formaten zurück....