Datenbanktests mit DbUnit

Datenbanktests mit DbUnit

Der Einsatz von Datenbanken gehört zum Alltag vieler Software-Entwickler. Möchte man Code testen, der auf die Datenbank zugreift, tut man sich allerdings häufig schwer. Wie lässt sich ein solcher Test überhaupt automatisieren? Wie schafft man es, dass der Test trotz echtem Datenbankzugriff schnell läuft und stabil bleibt?

Neben Tests der Datenbankschicht stellen automatisierte Integrationstests/Systemtests eine weitere Herausforderung dar. Vor Ausführung eines solchen Tests muss die Datenbank in einen bestimmten, bekannten Zustand versetzt werden. Ansonsten sind derartige Tests sehr fragil und erfordern einen hohen Wartungsaufwand, da sie häufig aufgrund einer veränderten Datenkonstellation fehlschlagen.

DbUnit bietet hierfür gute Lösungsansätze, es erlaubt etwa den Im- und Export von Datensätzen sowie ganzer Tabellen aus bzw. in XML-Dateien. Dieser datei-basierte Ansatz hat allerdings einige Nachteile. Zum einen ist in einem Test nicht direkt ersichtlich, welche Daten in die Datenbank geladen werden. Zum anderen müssen alle XML-Dateien von Hand angepasst werden, wenn sich das Datenbankschema ändert.

Dieser Vortrag stellt eine Alternative zum datei-basierten Ansatz vor, der dennoch die Stärken von DbUnit nutzt. Statt XML-Dateien zu editieren, erfolgt das Zusammenbauen eines DataSets direkt im Code unter Verwendung des Builder-Patterns. Weiter wird erläutert, wie etwa Umbenennungen oder die Einführung neuer Spalten über Refactorings damit ohne viel Aufwand zu erledigen sind.

956c7d246841e8507a1e1b96842994db?s=128

Marc Philipp

April 02, 2012
Tweet

Transcript

  1. Datenbanktests mit DbUnit Marc Philipp andrena objects ag ObjektForum Karlsruhe,

    2. April 2012 E-Mail marc@andrena.de Twitter @marcphilipp Blog http://www.marcphilipp.de
  2. Warum Datenbanktests? Datenbanken für die meisten Anwendungen unverzichtbar Konsistenz der

    Daten kritisch Komplexe Datenbank-Queries http://www.flickr.com/photos/14804582@N08/2111269218/ Datenbanktests mit DbUnit 2
  3. Warum keine Datenbanktests? Schwieriges Setup Fragile Tests Langsame Ausführung Datenbanktests

    mit DbUnit 3
  4. Beispiel Zu testende Klasse: PersonRepository public class Person { public

    Person(String firstName, String lastName, int age) { ... } } public class PersonRepository { public Person findPersonByFirstName(String name) { ... return person; } } Datenbanktests mit DbUnit 4
  5. Test für PersonRepository @Test public void findsAndReadsExistingPersonByFirstName() { PersonRepository repository

    = new PersonRepository(dataSource()); Person charlie = repository.findPersonByFirstName("Charlie"); assertThat(charlie.getFirstName(), is("Charlie")); assertThat(charlie.getLastName(), is("Brown")); assertThat(charlie.getAge(), is(42)); } Datenbanktests mit DbUnit 5
  6. Test für PersonRepository @Test public void findsAndReadsExistingPersonByFirstName() { PersonRepository repository

    = new PersonRepository(dataSource()); Person charlie = repository.findPersonByFirstName("Charlie"); assertThat(charlie.getFirstName(), is("Charlie")); assertThat(charlie.getLastName(), is("Brown")); assertThat(charlie.getAge(), is(42)); } Was fehlt? Datenbanktests mit DbUnit 6
  7. Das Setup fehlt! Damit der Test dauerhaft läuft, muss die

    Datenbank vor dem Test in einen bekannten Zustand gebracht werden. Datenbanktests mit DbUnit 7
  8. Das Setup fehlt! Damit der Test dauerhaft läuft, muss die

    Datenbank vor dem Test in einen bekannten Zustand gebracht werden. Datenbanktests mit DbUnit 8
  9. Das Setup fehlt! Damit der Test dauerhaft läuft, muss die

    Datenbank vor dem Test in einen bekannten Zustand gebracht werden. Datenbanktests mit DbUnit 9
  10. Das Setup fehlt! Damit der Test dauerhaft läuft, muss die

    Datenbank vor dem Test in einen bekannten Zustand gebracht werden. Datenbanktests mit DbUnit 10
  11. Erster Versuch: Plain JDBC http://www.flickr.com/photos/library_of_congress/2179123671/

  12. Plain JDBC @Before public class PlainJdbcDatabaseTest { private Connection connection;

    @Before public void insertRows() throws Exception { connection = DriverManager.getConnection(JDBC_URL, USER, PASSWORD); cleanPersonTable(); insert(new Person("Bob", "Doe", 18)); insert(new Person("Alice", "Foo", 23)); insert(new Person("Charlie", "Brown", 42)); } } Datenbanktests mit DbUnit 12
  13. Plain JDBC Tabelle leeren private void cleanPersonTable() throws SQLException {

    Statement statement = null; try { statement = connection.createStatement(); statement.executeUpdate("DELETE FROM PERSON"); } finally { if (statement != null) { try { statement.close(); } catch (SQLException ignore) { } } } } Datenbanktests mit DbUnit 13
  14. Plain JDBC Zeilen einfügen private void insert(Person person) throws SQLException

    { PreparedStatement statement = null; try { statement = connection.prepareStatement("INSERT INTO PERSON ( NAME, LAST_NAME, AGE) VALUES (?, ?, ?)"); statement.setString(1, person.getFirstName()); statement.setString(2, person.getLastName()); statement.setInt(3, person.getAge()); statement.executeUpdate(); } finally { if (statement != null) { try { statement.close(); } catch (SQLException ignore) { } } } } Datenbanktests mit DbUnit 14
  15. Plain JDBC Verbindung schließen @After public void closeConnection() { if

    (connection != null) { try { connection.close(); } catch (SQLException ignore) { } } } Datenbanktests mit DbUnit 15
  16. Funktioniert, aber. . . Viel Code Wiederholt sich in jedem

    Test. . . oder ergibt zweite Datenbank-Schicht Benutzt SQL und ist damit Datenbank-abhängig Datenbanktests mit DbUnit 16
  17. DbUnit http://en.wikipedia.org/wiki/File:Hexagon_nuts.jpg

  18. Definition eines DataSet Ein DataSet definiert eine Menge von Datenbanktabellen

    und -zeilen. <dataset> <PERSON NAME="Bob" LAST_NAME="Doe" AGE="18"/> <PERSON NAME="Alice" LAST_NAME="Foo" AGE="23"/> <PERSON NAME="Charlie" LAST_NAME="Brown" AGE="42"/> </dataset> Datenbanktests mit DbUnit 18
  19. DataSet einlesen & importieren public class XmlDatabaseTest { @Before public

    void importDataSet() throws Exception { IDataSet dataSet = readDataSet(); cleanlyInsert(dataSet); } private IDataSet readDataSet() throws Exception { return new FlatXmlDataSetBuilder().build( new File("dataset.xml")); } private void cleanlyInsert(IDataSet dataSet) throws Exception { IDatabaseTester databaseTester = new JdbcDatabaseTester( JDBC_DRIVER, JDBC_URL, USER, PASSWORD); databaseTester.setSetUpOperation(DatabaseOperation. CLEAN_INSERT); databaseTester.setDataSet(dataSet); databaseTester.onSetup(); } ... Datenbanktests mit DbUnit 19
  20. Plain JDBC vs. DbUnit Vorteile von DbUnit Kein SQL im

    Test Datenbank-unabhängig Schwächen von DbUnit DataSet-Definition in externer Datei XML – hoher Aufwand bei Schemaänderungen Datenbanktests mit DbUnit 20
  21. Plain JDBC vs. DbUnit Vorteile von DbUnit Kein SQL im

    Test Datenbank-unabhängig Schwächen von DbUnit DataSet-Definition in externer Datei XML – hoher Aufwand bei Schemaänderungen Datenbanktests mit DbUnit 21
  22. Plain JDBC vs. DbUnit Vorteile von DbUnit Kein SQL im

    Test Datenbank-unabhängig Schwächen von DbUnit DataSet-Definition in externer Datei XML – hoher Aufwand bei Schemaänderungen Datenbanktests mit DbUnit 22
  23. DataSetBuilder http://www.flickr.com/photos/striatic/2518868119/

  24. DataSetBuilder Idee Keine externen XML-Dateien Erzeuge DataSet direkt im Code

    DataSetBuilder builder = new DataSetBuilder(); builder .newRow("PERSON") // Table .with("NAME", "Bob") // Column, Value .add(); IDataSet dataset = builder.build(); Datenbanktests mit DbUnit 24
  25. DataSetBuilder Idee Keine externen XML-Dateien Erzeuge DataSet direkt im Code

    DataSetBuilder builder = new DataSetBuilder(); builder .newRow("PERSON") // Table .with("NAME", "Bob") // Column, Value .add(); IDataSet dataset = builder.build(); Datenbanktests mit DbUnit 25
  26. XML vs. Code <dataset> <PERSON NAME="Bob" LAST_NAME="Doe" AGE="18"/> <PERSON NAME="Alice"

    LAST_NAME="Foo" AGE="23"/> <PERSON NAME="Charlie" LAST_NAME="Brown" AGE="42"/> </dataset> entspricht builder.newRow("PERSON").with("NAME", "Bob") .with("LAST_NAME", "Doe").with("AGE", 18).add(); builder.newRow("PERSON").with("NAME", "Alice") .with("LAST_NAME", "Foo").with("AGE", 23).add(); builder.newRow("PERSON").with("NAME", "Charlie") .with("LAST_NAME", "Brown").with("AGE", 42).add(); Datenbanktests mit DbUnit 26
  27. Benutzung im Test public class BuilderDatabaseTest { @Before public void

    importDataSet() throws Exception { IDataSet dataSet = buildDataSet(); cleanlyInsert(dataSet); } private IDataSet buildDataSet() throws Exception { DataSetBuilder builder = new DataSetBuilder(); builder.newRow("PERSON").with("NAME", "Bob") .with("LAST_NAME", "Doe").with("AGE", 18).add(); builder.newRow("PERSON").with("NAME", "Alice") .with("LAST_NAME", "Foo").with("AGE", 23).add(); builder.newRow("PERSON").with("NAME", "Charlie") .with("LAST_NAME", "Brown").with("AGE", 42).add(); return builder.build(); } ... Datenbanktests mit DbUnit 27
  28. ColumnSpec<T> Typsichere Alternative Ersetze builder.newRow("PERSON").with("NAME", "Bob").with("AGE", 18).add(); durch ColumnSpec<String> NAME

    = ColumnSpec.newColumn("NAME"); ColumnSpec<Integer> AGE = ColumnSpec.newColumn("AGE"); builder.newRow("PERSON").with(NAME, "Bob").with(AGE, 18).add(); Compile Fehler: builder.newRow("PERSON").with(AGE, "Bob").add(); Datenbanktests mit DbUnit 28
  29. ColumnSpec<T> Typsichere Alternative Ersetze builder.newRow("PERSON").with("NAME", "Bob").with("AGE", 18).add(); durch ColumnSpec<String> NAME

    = ColumnSpec.newColumn("NAME"); ColumnSpec<Integer> AGE = ColumnSpec.newColumn("AGE"); builder.newRow("PERSON").with(NAME, "Bob").with(AGE, 18).add(); Compile Fehler: builder.newRow("PERSON").with(AGE, "Bob").add(); Datenbanktests mit DbUnit 29
  30. Einfache Spaltenumbenennungen Ändert sich ein Spaltenname, muss nur eine Konstante

    geändert werden. class PersonTable { static final ColumnSpec<String> NAME = newColumn("NAME"); static final ColumnSpec<String> LAST_NAME = newColumn("LAST_NAME"); static final ColumnSpec<Integer> AGE = newColumn("AGE"); } Datenbanktests mit DbUnit 30
  31. Vorteile mit DataSetBuilder Keine XML-Dateien DataSet direkt im Code ersichtlich

    Einfaches Anpassen von Spaltennamen Ungelöste Probleme: alle Tests anpassen? Was passiert bei neuen Spalten? Was, wenn Spalten gelöscht werden? Datenbanktests mit DbUnit 31
  32. Vorteile mit DataSetBuilder Keine XML-Dateien DataSet direkt im Code ersichtlich

    Einfaches Anpassen von Spaltennamen Ungelöste Probleme: alle Tests anpassen? Was passiert bei neuen Spalten? Was, wenn Spalten gelöscht werden? Datenbanktests mit DbUnit 32
  33. Wrap the builder http://www.flickr.com/photos/jamieanne/5583561331/

  34. Eigener Builder Idee Eigener Builder für das Erstellen von Person-Zeilen

    Benutze DataSetBuilder intern Datenbanktests mit DbUnit 34
  35. Eigener Builder Umsetzung public class PersonRowBuilder { ... private final

    DataSetBuilder dataSetBuilder; private String firstName, lastName; private int age; public PersonRowBuilder(DataSetBuilder dataSetBuilder) { this.dataSetBuilder = dataSetBuilder; } public PersonRowBuilder withFirstName(String firstName) { this.firstName = firstName; return this; } ... public void add() throws DataSetException { dataSetBuilder.newRow(TABLE_NAME).with(NAME, firstName) .with(LAST_NAME, lastName).with(AGE, age).add(); } } Datenbanktests mit DbUnit 35
  36. Eigener Builder Benutzung im Test public class CustomRowBuilderDatabaseTest { @Before

    public void importDataSet() throws Exception { IDataSet dataSet = buildDataSet(); cleanlyInsert(dataSet); } private IDataSet buildDataSet() throws Exception { DataSetBuilder builder = new DataSetBuilder(); new PersonRowBuilder(builder).withFirstName("Bob"). withLastName("Doe").withAge(18).add(); PersonRowBuilder.newPerson(builder).withFirstName("Alice"). withLastName("Foo").withAge(23).add(); newPerson(builder).withFirstName("Charlie").withLastName(" Brown").withAge(42).add(); return builder.build(); } ... Datenbanktests mit DbUnit 36
  37. Änderungen am Datenbankschema 1. Zusätzliche Spalte 2. Spalte entfernen Datenbanktests

    mit DbUnit 37
  38. Wechsel zu Eclipse. . .

  39. Robust gegen Änderungen am Schema Spalten-Umbenennungen Zusätzliche Spalten Spalten-Löschungen Datenbanktests

    mit DbUnit 39
  40. Warum keine Datenbanktests? Schwieriges Setup Fragile Tests Langsame Ausführung Datenbanktests

    mit DbUnit 40
  41. Warum keine Datenbanktests? Schwieriges Setup Fragile Tests Langsame Ausführung Datenbanktests

    mit DbUnit 41
  42. Warum keine Datenbanktests? Schwieriges Setup Fragile Tests Langsame Ausführung Datenbanktests

    mit DbUnit 42
  43. Warum keine Datenbanktests? Schwieriges Setup Fragile Tests Langsame Ausführung Datenbanktests

    mit DbUnit 43
  44. Warum keine Datenbanktests? Schwieriges Setup Fragile Tests Langsame Ausführung Datenbanktests

    mit DbUnit 44
  45. Warum keine Datenbanktests? Schwieriges Setup Fragile Tests Langsame Ausführung Datenbanktests

    mit DbUnit 45
  46. Mehrere Datenbanken http://www.flickr.com/photos/yakobusan/2436481628/

  47. Übliches Setup

  48. In-Memory-Datenbanken brauchen Schema Beispiel: H2 Schema erzeugen, bevor Datenbank mit

    DbUnit befüllt wird: @BeforeClass public static void createSchema() throws Exception { if (JDBC_URL.startsWith("jdbc:h2:mem:")) { RunScript.execute(JDBC_URL, USER, PASSWORD, "schema.sql", UTF8, false); } } RunScript ist eine von H2 bereitgestellte Klasse. Datenbanktests mit DbUnit 48
  49. Übliches Setup

  50. Datenbank-spezifische Tests Trotz O/R-Mapper etc. lässt sich DB-abhängiger Code oft

    nicht ganz vermeiden. . . @Test @OracleOnly public void oracleSpecificStuff() throws Exception { System.err.println("Hello from Delphi!"); // call a stored procedure etc. } Datenbanktests mit DbUnit 50
  51. Datenbank-spezifische Tests Trotz O/R-Mapper etc. lässt sich DB-abhängiger Code oft

    nicht ganz vermeiden. . . @Test @OracleOnly public void oracleSpecificStuff() throws Exception { System.err.println("Hello from Delphi!"); // call a stored procedure etc. } Datenbanktests mit DbUnit 51
  52. Exkurs: JUnit Rules http://www.flickr.com/photos/phunk/357358126/

  53. Benutzung einer Rule in JUnit public class CustomRowBuilderDatabaseTestWithRules { @Rule

    public TestRule onlyRunOracleTestsOnOracle = new OnlyRunOracleTestsOnOracle(dataSource()); @Test @OracleOnly public void oracleSpecificStuff() throws Exception { System.err.println("Hello from Delphi!"); // call a stored procedure etc. } } @Retention(RUNTIME) public @interface OracleOnly {} Datenbanktests mit DbUnit 53
  54. Definition einer Rule in JUnit public class OnlyRunOracleTestsOnOracle implements TestRule

    { private static final class EmptyStatement extends Statement { @Override public void evaluate() { /* do nothing */ } } public OnlyRunOracleTestsOnOracle(DataSource dataSource) { ... } @Override public Statement apply(Statement base, Description description) { if (shouldSkipTest(description)) { return new EmptyStatement(); } return base; } private boolean shouldSkipTest(Description description) { return hasAnnotation(description, OracleOnly.class) && !isOracleDataSource(); } ... Datenbanktests mit DbUnit 54
  55. Kurze Zeit später. . .

  56. Drei Rules public class CustomRowBuilderDatabaseTestWithRules { @ClassRule public static TestRule

    schema = new CreateSchemaIfNecessary(dataSource(), "schema.sql"); @Rule public TestRule importDataSet = new ImportDataSet( dataSource(), this); @Rule public TestRule onlyRunOracleTestsOnOracle = new OnlyRunOracleTestsOnOracle(dataSource()); @DataSet public IDataSet dataSet() throws DataSetException { ... return builder.build(); } @Test public void findsAndReadsExistingPersonByFirstName() { ... } ... } Datenbanktests mit DbUnit 56
  57. Aus 3 mach 1 Benutzung public class CustomRowBuilderDatabaseTestWithSingleRule { @Rule

    public TestRule db = new PrepareDatabase(dataSource(), this); @DataSet public IDataSet dataSet() throws DataSetException { ... return builder.build(); } @Test public void findsAndReadsExistingPersonByFirstName() { ... } ... } Datenbanktests mit DbUnit 57
  58. Aus 3 mach 1 Definition public class PrepareDatabase implements TestRule

    { ... public PrepareDatabase(DataSource dataSource, Object test) { this.dataSource = dataSource; this.test = test; } @Override public Statement apply(Statement base, Description description) { return chain().apply(base, description); } private RuleChain chain() { return RuleChain .outerRule(new OnlyRunOracleTestsOnOracle(dataSource)) .around(new CreateSchemaIfNecessary(dataSource, "schema.sql")) .around(new ImportDataSet(dataSource, test)); } } Datenbanktests mit DbUnit 58
  59. Zusammenfassung

  60. Warum keine Datenbanktests? Schwieriges Setup Fragile Tests Langsame Ausführung Datenbanktests

    mit DbUnit 60
  61. Warum keine Datenbanktests? Schwieriges Setup Fragile Tests Langsame Ausführung Datenbanktests

    mit DbUnit 61
  62. Warum keine Datenbanktests? Schwieriges Setup Fragile Tests Langsame Ausführung Datenbanktests

    mit DbUnit 62
  63. Warum Datenbanktests? Datenbanken für die meisten Anwendungen unverzichtbar Konsistenz der

    Daten kritisch Komplexe Datenbank-Queries http://www.flickr.com/photos/14804582@N08/2111269218/ Datenbanktests mit DbUnit 63
  64. Testet euren DB-Code! http://www.dbunit.org/ http://www.junit.org/ https://github.com/marcphilipp/dbunit-examples https://github.com/marcphilipp/dbunit-datasetbuilder Danke! E-Mail marc@andrena.de

    Twitter @marcphilipp Blog http://www.marcphilipp.de Datenbanktests mit DbUnit 64