dbUnit in 5 Minutes

In 5 minutes you’ll write a database test that prepares data, exercises your code, and verifies the result.

Prerequisites

  • JDK 17 or newer to build and run this tutorial’s test (JUnit 6 requires it). The dbUnit library itself still targets Java 8 bytecode, so JDK 8 remains enough if you only use its core API without JUnit’s test-support classes.
  • JUnit 5 or 6 (org.junit.jupiter:junit-jupiter) on your test classpath.
  • An in-memory database for the example — this tutorial uses H2 so there’s nothing to install or run.

Step 1 — Add the Dependency

<dependency>
  <groupId>org.dbunit</groupId>
  <artifactId>dbunit</artifactId>
  <version>${dbunitVersion}</version>
  <scope>test</scope>
</dependency>

See Maven Repositories for Gradle coordinates and full release/snapshot details. Also add H2 to your test classpath:

<dependency>
  <groupId>com.h2database</groupId>
  <artifactId>h2</artifactId>
  <version>1.4.200</version>
  <scope>test</scope>
</dependency>

Step 2 — Write a Prep Dataset

The prep dataset is the data your test needs in the database before it runs. This one puts two rows into an account table (classpath resource /dbunit/account-prep.xml):

<?xml version='1.0' encoding='UTF-8'?>
<dataset>
    <account id="1" name="Alice" balance="100"/>
    <account id="2" name="Bob" balance="200"/>
</dataset>

Step 3 — Write an Expected Dataset

The expected dataset is the table state your test verifies against, after the code under test runs. This test withdraws 50 from account 1, so its balance drops to 50; account 2 is untouched (classpath resource /dbunit/account-expected.xml):

<?xml version='1.0' encoding='UTF-8'?>
<dataset>
    <account id="1" name="Alice" balance="50"/>
    <account id="2" name="Bob" balance="200"/>
</dataset>

Step 4 — Write the Test

Use DefaultPrepAndExpectedTestCase composed with a JdbcDatabaseTester — this is the recommended composition-based style (see the IDatabaseTester guide for why it’s preferred over extending DBTestCase).

package com.example;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement;

import org.dbunit.DefaultPrepAndExpectedTestCase;
import org.dbunit.JdbcDatabaseTester;
import org.dbunit.PrepAndExpectedTestCase;
import org.dbunit.VerifyTableDefinition;
import org.dbunit.operation.DatabaseOperation;
import org.dbunit.util.fileloader.DataFileLoader;
import org.dbunit.util.fileloader.FlatXmlDataFileLoader;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

class AccountWithdrawalTest
{
    private static final String DRIVER_CLASS = "org.h2.Driver";
    private static final String CONNECTION_URL = "jdbc:h2:mem:dbunit5min;DB_CLOSE_DELAY=-1";

    private static final String DATA_DIR = "/dbunit/";
    private static final String ACCOUNT_PREP = DATA_DIR + "account-prep.xml";
    private static final String ACCOUNT_EXPECTED = DATA_DIR + "account-expected.xml";

    @BeforeAll
    static void createSchema() throws Exception
    {
        try (Connection connection = DriverManager.getConnection(CONNECTION_URL);
                Statement statement = connection.createStatement())
        {
            statement.execute("CREATE TABLE account (id INT PRIMARY KEY, name VARCHAR(50), balance INT)");
        }
    }

    @Test
    void testWithdraw_sufficientBalance_decrementsBalance() throws Exception
    {
        final JdbcDatabaseTester databaseTester = new JdbcDatabaseTester(DRIVER_CLASS, CONNECTION_URL);
        databaseTester.setTearDownOperation(DatabaseOperation.DELETE_ALL);
        final DataFileLoader dataFileLoader = new FlatXmlDataFileLoader();
        final PrepAndExpectedTestCase testCase =
                new DefaultPrepAndExpectedTestCase(dataFileLoader, databaseTester);

        final VerifyTableDefinition[] verifyTables = { new VerifyTableDefinition("account", null) };
        final String[] prepDataFiles = { ACCOUNT_PREP };
        final String[] expectedDataFiles = { ACCOUNT_EXPECTED };

        testCase.runTest(verifyTables, prepDataFiles, expectedDataFiles, () -> {
            // the code under test: withdraw 50 from account 1
            try (Connection connection = DriverManager.getConnection(CONNECTION_URL);
                    Statement statement = connection.createStatement())
            {
                statement.execute("UPDATE account SET balance = balance - 50 WHERE id = 1");
            }
            return null;
        });
    }
}

The IDatabaseTester composing the test case supplies the setup/teardown operations: it defaults to CLEAN_INSERT (truncate + insert) for setup and NONE for teardown, so the explicit setTearDownOperation(DELETE_ALL) above is what makes cleanup actually happen. Both are configurable, see IDatabaseTester and Database Operations.

Step 5 — Run It

mvn test

What Just Happened

runTest() loaded account-prep.xml and inserted its rows, ran your test step (the withdrawal), verified the account table matches account-expected.xml, and then cleaned up the table — all in one call.

The Same Test, Annotation-Driven

Steps 2 and 3 don’t change; step 4 shrinks. On JUnit 5/6 (Jupiter), org.dbunit.annotation can declare the same prep/expected test declaratively instead of calling runTest(). createSchema() and the DRIVER_CLASS/CONNECTION_URL/ACCOUNT_PREP/ACCOUNT_EXPECTED constants stay exactly as in step 4 above; the rest of the class is rewritten below to use @DbUnitTest, @DbUnitTester, and the lifecycle annotations in place of the manual PrepAndExpectedTestCase wiring:

@DbUnitTest
class AccountWithdrawalTest
{
    // ...DRIVER_CLASS, CONNECTION_URL, ACCOUNT_PREP, ACCOUNT_EXPECTED,
    // and @BeforeAll createSchema() unchanged from above...

    @DbUnitTester
    static final IDatabaseTester databaseTester;

    static
    {
        try
        {
            databaseTester = new JdbcDatabaseTester(DRIVER_CLASS, CONNECTION_URL);
        }
        catch (ClassNotFoundException e)
        {
            throw new ExceptionInInitializerError(e);
        }
    }

    @Test
    @DbUnitPrep(ACCOUNT_PREP)
    @DbUnitExpected(value = ACCOUNT_EXPECTED, verifyTables = "account")
    @DbUnitTearDown(operation = DbUnitOperation.DELETE_ALL)
    void testWithdraw_sufficientBalance_decrementsBalance() throws Exception
    {
        try (Connection connection = DriverManager.getConnection(CONNECTION_URL);
                Statement statement = connection.createStatement())
        {
            statement.execute("UPDATE account SET balance = balance - 50 WHERE id = 1");
        }
    }
}

databaseTester.setTearDownOperation(DELETE_ALL) from step 4 becomes @DbUnitTearDown(operation = DbUnitOperation.DELETE_ALL) — declared per method, or once per class when every method needs the same cleanup. Leaving it off entirely means no teardown runs at all (NONE is the actual default), not some baseline cleanup "for free" — declare it whenever a test leaves rows behind. See DbUnit Annotations for the full reference.

Where to Go Next