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

    View Slide

  2. Warum Datenbanktests?
    Datenbanken für die
    meisten Anwendungen
    unverzichtbar
    Konsistenz der Daten
    kritisch
    Komplexe
    Datenbank-Queries
    http://www.flickr.com/photos/[email protected]/2111269218/
    Datenbanktests mit DbUnit 2

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  11. Erster Versuch: Plain JDBC
    http://www.flickr.com/photos/library_of_congress/2179123671/

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  15. Plain JDBC
    Verbindung schließen
    @After
    public void closeConnection() {
    if (connection != null) {
    try {
    connection.close();
    } catch (SQLException ignore) {
    }
    }
    }
    Datenbanktests mit DbUnit 15

    View Slide

  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

    View Slide

  17. DbUnit
    http://en.wikipedia.org/wiki/File:Hexagon_nuts.jpg

    View Slide

  18. Definition eines DataSet
    Ein DataSet definiert eine Menge von Datenbanktabellen
    und -zeilen.





    Datenbanktests mit DbUnit 18

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  23. DataSetBuilder
    http://www.flickr.com/photos/striatic/2518868119/

    View Slide

  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

    View Slide

  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

    View Slide

  26. XML vs. Code





    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

    View Slide

  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

    View Slide

  28. ColumnSpec
    Typsichere Alternative
    Ersetze
    builder.newRow("PERSON").with("NAME", "Bob").with("AGE", 18).add();
    durch
    ColumnSpec NAME = ColumnSpec.newColumn("NAME");
    ColumnSpec 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

    View Slide

  29. ColumnSpec
    Typsichere Alternative
    Ersetze
    builder.newRow("PERSON").with("NAME", "Bob").with("AGE", 18).add();
    durch
    ColumnSpec NAME = ColumnSpec.newColumn("NAME");
    ColumnSpec 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

    View Slide

  30. Einfache Spaltenumbenennungen
    Ändert sich ein Spaltenname, muss nur eine Konstante
    geändert werden.
    class PersonTable {
    static final ColumnSpec NAME = newColumn("NAME");
    static final ColumnSpec LAST_NAME
    = newColumn("LAST_NAME");
    static final ColumnSpec AGE = newColumn("AGE");
    }
    Datenbanktests mit DbUnit 30

    View Slide

  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

    View Slide

  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

    View Slide

  33. Wrap the builder
    http://www.flickr.com/photos/jamieanne/5583561331/

    View Slide

  34. Eigener Builder
    Idee
    Eigener Builder für das Erstellen von Person-Zeilen
    Benutze DataSetBuilder intern
    Datenbanktests mit DbUnit 34

    View Slide

  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

    View Slide

  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

    View Slide

  37. Änderungen am Datenbankschema
    1. Zusätzliche Spalte
    2. Spalte entfernen
    Datenbanktests mit DbUnit 37

    View Slide

  38. Wechsel zu Eclipse. . .

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  47. Übliches Setup

    View Slide

  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

    View Slide

  49. Übliches Setup

    View Slide

  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

    View Slide

  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

    View Slide

  52. Exkurs: JUnit Rules
    http://www.flickr.com/photos/phunk/357358126/

    View Slide

  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

    View Slide

  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

    View Slide

  55. Kurze Zeit später. . .

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  59. Zusammenfassung

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  63. Warum Datenbanktests?
    Datenbanken für die
    meisten Anwendungen
    unverzichtbar
    Konsistenz der Daten
    kritisch
    Komplexe
    Datenbank-Queries
    http://www.flickr.com/photos/[email protected]/2111269218/
    Datenbanktests mit DbUnit 63

    View Slide

  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 [email protected]
    Twitter @marcphilipp
    Blog http://www.marcphilipp.de
    Datenbanktests mit DbUnit 64

    View Slide