Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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.

Marc Philipp

April 02, 2012
Tweet

More Decks by Marc Philipp

Other Decks in Programming

Transcript

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

    2. April 2012 E-Mail [email protected] 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. 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
  4. 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
  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)); } Was fehlt? Datenbanktests mit DbUnit 6
  6. 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
  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 8
  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 9
  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 10
  10. 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
  11. 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
  12. 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
  13. Plain JDBC Verbindung schließen @After public void closeConnection() { if

    (connection != null) { try { connection.close(); } catch (SQLException ignore) { } } } Datenbanktests mit DbUnit 15
  14. 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
  15. 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
  16. 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
  17. 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
  18. 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
  19. 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
  20. 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
  21. 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
  22. 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
  23. 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
  24. 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
  25. 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
  26. 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
  27. 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
  28. 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
  29. Eigener Builder Idee Eigener Builder für das Erstellen von Person-Zeilen

    Benutze DataSetBuilder intern Datenbanktests mit DbUnit 34
  30. 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
  31. 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
  32. 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
  33. 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
  34. 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
  35. 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
  36. 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
  37. 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
  38. 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
  39. 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
  40. 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