DbUnit Java Database Testing: Dataset Management and Spring Boot Integration

DbUnit Java Database Testing: Dataset Management and Spring Boot Integration

Java applications that talk to a database face a persistent testing problem: how do you test database interactions without making tests slow, fragile, or dependent on production data? DbUnit solves this by providing a framework for managing test datasets — populating tables before tests and verifying their state afterward.

This guide covers DbUnit fundamentals, the modern Database Rider extension, and integrating database tests into a Spring Boot project.

What Is DbUnit?

DbUnit is a JUnit extension (works with JUnit 4 and 5) focused on database-driven tests. Its core capabilities:

  • Dataset management — define test data in XML, JSON, CSV, or Excel files
  • Database state setup — populate tables before each test from a dataset
  • Database state assertion — compare actual table contents against an expected dataset
  • Clean insert operations — truncate and repopulate tables between tests for isolation

DbUnit doesn't replace your JPA or JDBC layer — it works alongside them to control the database state around your tests.

Adding DbUnit to Your Project

Maven

<dependencies>
  <!-- DbUnit core -->
  <dependency>
    <groupId>org.dbunit</groupId>
    <artifactId>dbunit</artifactId>
    <version>2.7.3</version>
    <scope>test</scope>
  </dependency>

  <!-- Database Rider (modern DbUnit wrapper) -->
  <dependency>
    <groupId>com.github.database-rider</groupId>
    <artifactId>rider-junit5</artifactId>
    <version>1.42.0</version>
    <scope>test</scope>
  </dependency>

  <!-- Spring Boot integration -->
  <dependency>
    <groupId>com.github.database-rider</groupId>
    <artifactId>rider-spring</artifactId>
    <version>1.42.0</version>
    <scope>test</scope>
  </dependency>
</dependencies>

Gradle

testImplementation 'org.dbunit:dbunit:2.7.3'
testImplementation 'com.github.database-rider:rider-junit5:1.42.0'
testImplementation 'com.github.database-rider:rider-spring:1.42.0'

Defining Datasets

Datasets describe the initial state of your database tables. DbUnit supports several formats.

XML Dataset (Flat Format)

The most common format — each row is an XML element:

<!-- src/test/resources/datasets/users.xml -->
<?xml version='1.0' encoding='UTF-8'?>
<dataset>
    <users id="1" email="alice@example.com" name="Alice Smith" status="ACTIVE" />
    <users id="2" email="bob@example.com" name="Bob Jones" status="ACTIVE" />
    <users id="3" email="carol@example.com" name="Carol White" status="INACTIVE" />
    
    <orders id="1" user_id="1" total_amount="150.00" status="COMPLETED" />
    <orders id="2" user_id="1" total_amount="75.50" status="PENDING" />
    <orders id="3" user_id="2" total_amount="200.00" status="CANCELLED" />
</dataset>

JSON Dataset (Database Rider)

Database Rider supports JSON datasets, which many teams find more readable:

{
  "users": [
    {"id": 1, "email": "alice@example.com", "name": "Alice Smith", "status": "ACTIVE"},
    {"id": 2, "email": "bob@example.com", "name": "Bob Jones", "status": "ACTIVE"}
  ],
  "orders": [
    {"id": 1, "user_id": 1, "total_amount": 150.00, "status": "COMPLETED"},
    {"id": 2, "user_id": 1, "total_amount": 75.50, "status": "PENDING"}
  ]
}

YAML Dataset

users:
  - id: 1
    email: alice@example.com
    name: Alice Smith
    status: ACTIVE
  - id: 2
    email: bob@example.com
    name: Bob Jones
    status: ACTIVE

orders:
  - id: 1
    user_id: 1
    total_amount: 150.00
    status: COMPLETED

Basic DbUnit Tests

Here's a raw DbUnit test without any wrapper frameworks:

import org.dbunit.DatabaseTestCase;
import org.dbunit.database.DatabaseConnection;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSetBuilder;
import org.dbunit.operation.DatabaseOperation;

public class UserRepositoryTest extends DatabaseTestCase {

    @Override
    protected IDatabaseConnection getConnection() throws Exception {
        Connection conn = DriverManager.getConnection(
            "jdbc:h2:mem:testdb", "sa", ""
        );
        return new DatabaseConnection(conn);
    }

    @Override
    protected IDataSet getDataSet() throws Exception {
        return new FlatXmlDataSetBuilder()
            .build(getClass().getResourceAsStream("/datasets/users.xml"));
    }

    public void testFindActiveUsers() throws Exception {
        // Database is already populated from users.xml
        UserRepository repo = new UserRepository(getConnection().getConnection());
        List<User> activeUsers = repo.findByStatus("ACTIVE");
        
        assertEquals(2, activeUsers.size());
    }
}

Database Rider (Modern Approach)

Raw DbUnit is verbose. Database Rider is a modern wrapper that integrates with JUnit 5 annotations and dramatically reduces boilerplate.

Basic Database Rider Test

import com.github.database.rider.core.api.connection.ConnectionHolder;
import com.github.database.rider.core.api.dataset.DataSet;
import com.github.database.rider.junit5.api.DBRider;
import org.junit.jupiter.api.Test;

@DBRider
class UserRepositoryTest {

    @ConnectionHolder
    static ConnectionHolder connectionHolder = () ->
        DriverManager.getConnection("jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1", "sa", "");

    @Test
    @DataSet("datasets/users.yml")
    void findActiveUsers() {
        UserRepository repo = new UserRepository(connectionHolder.getConnection());
        List<User> users = repo.findByStatus("ACTIVE");
        
        assertThat(users).hasSize(2);
        assertThat(users).extracting(User::getEmail)
            .containsExactlyInAnyOrder("alice@example.com", "bob@example.com");
    }

    @Test
    @DataSet("datasets/users.yml")
    @ExpectedDataSet("datasets/expected_users_after_deactivation.yml")
    void deactivateUser() {
        UserRepository repo = new UserRepository(connectionHolder.getConnection());
        repo.deactivate(1L);
        
        // @ExpectedDataSet verifies table state automatically after the test
    }
}

Dataset Operations

Control how DbUnit loads data with the cleanBefore and strategy options:

@DataSet(
    value = "datasets/orders.yml",
    cleanBefore = true,           // truncate affected tables before loading
    strategy = SeedStrategy.CLEAN_INSERT  // DELETE all rows, then INSERT
)
void testOrderProcessing() {
    // ...
}

Available strategies:

  • CLEAN_INSERT — delete all rows from affected tables, then insert (default)
  • INSERT — insert without deleting (fails if rows conflict)
  • REFRESH — update existing rows, insert new ones
  • TRUNCATE_INSERT — use TRUNCATE instead of DELETE (faster but not transaction-safe)
  • DELETE — delete dataset rows (for teardown)
  • DELETE_ALL — delete all rows from affected tables

Spring Boot Integration

Configuration

// src/test/java/config/DatabaseRiderConfig.java
@Configuration
@Profile("test")
public class DatabaseRiderConfig {

    @Bean
    public ConnectionHolder connectionHolder(DataSource dataSource) throws SQLException {
        return dataSource::getConnection;
    }
}

Test with Spring Boot + Database Rider

@SpringBootTest
@DBRider
@ActiveProfiles("test")
@Transactional
class OrderServiceIntegrationTest {

    @Autowired
    private OrderService orderService;

    @Autowired
    private ConnectionHolder connectionHolder;

    @Test
    @DataSet("datasets/orders/pending_orders.yml")
    void processAllPendingOrders_shouldUpdateStatusToCompleted() {
        orderService.processAllPending();

        // Verify via repository
        List<Order> processed = orderService.findByStatus(OrderStatus.COMPLETED);
        assertThat(processed).hasSize(2);
    }

    @Test
    @DataSet("datasets/orders/empty.yml")
    void processAllPendingOrders_whenNoOrders_shouldNotThrow() {
        assertDoesNotThrow(() -> orderService.processAllPending());
    }

    @Test
    @DataSet("datasets/users/single_user.yml")
    @ExpectedDataSet("datasets/users/expected_after_update.yml")
    void updateUserProfile_shouldPersistChanges() {
        UserProfileRequest request = new UserProfileRequest("New Name", "new@email.com");
        userService.updateProfile(1L, request);
    }
}

H2 Test Database Setup

For fast, in-memory testing with Spring Boot:

# src/test/resources/application-test.properties
spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;MODE=MySQL
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=

spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true

Expected Datasets and Assertions

@ExpectedDataSet compares actual table contents to an expected dataset after the test completes:

# datasets/expected_order_after_cancellation.yml
orders:
  - id: 1
    status: CANCELLED
    cancelled_at: "[ignore]"  # ignore this column in comparison

Use [ignore] for columns with dynamic values like timestamps. Database Rider also supports column replacement expressions for partial matching.

Manual Assertions

When @ExpectedDataSet isn't flexible enough:

@Test
@DataSet("datasets/users.yml")
void createUser_shouldIncrementCount(ConnectionHolder connectionHolder) throws Exception {
    IDatabaseConnection dbConn = new DatabaseConnection(connectionHolder.getConnection());
    
    // Get initial count
    ITable beforeTable = dbConn.createTable("users");
    int initialCount = beforeTable.getRowCount();
    
    // Act
    userService.createUser("newuser@example.com", "New User");
    
    // Assert
    ITable afterTable = dbConn.createTable("users");
    assertEquals(initialCount + 1, afterTable.getRowCount());
    
    // Find the new row
    String newEmail = (String) afterTable.getValue(initialCount, "email");
    assertEquals("newuser@example.com", newEmail);
}

Handling Foreign Key Constraints

When loading datasets with foreign key relationships, order matters. DbUnit handles this with @DataSet(executeStatementsBefore = "..."):

@Test
@DataSet(
    value = "datasets/orders_with_users.yml",
    executeStatementsBefore = "SET FOREIGN_KEY_CHECKS=0",
    executeStatementsAfter = "SET FOREIGN_KEY_CHECKS=1"
)
void testOrderWithForeignKeys() {
    // ...
}

Or disable foreign key checks for the entire test class:

@DBUnit(disableConstraints = true)
@DBRider
class OrderTest {
    // Foreign key constraints disabled for all tests in this class
}

Dataset Builders for Dynamic Data

When static dataset files aren't flexible enough, build datasets programmatically:

@Test
@DataSet  // Empty dataset - we build it in code
void testWithDynamicData() throws Exception {
    DataSetBuilder builder = new DataSetBuilder();
    
    IDataSet dataSet = builder
        .table("users")
            .row().column("id", 1).column("email", "alice@test.com").column("status", "ACTIVE")
            .row().column("id", 2).column("email", "bob@test.com").column("status", "INACTIVE")
        .table("orders")
            .row().column("id", 1).column("user_id", 1).column("total_amount", 100.00)
        .build();
    
    DatabaseOperation.CLEAN_INSERT.execute(connection, dataSet);
    
    // Run your test...
}

Best Practices

Keep datasets small. A dataset with 3-5 rows per table is easier to understand and maintain than one with 50 rows. Tests should describe exactly what they need.

Name datasets descriptively. datasets/orders/two_pending_orders.yml is better than datasets/test_data.xml. The name should describe the scenario, not just the content.

One dataset per test scenario. Reusing the same large dataset across many tests creates hidden coupling — when you add a row for one test, you may break another that assumed a specific row count.

Use @DataSet(cleanBefore = true) for tests that care about exact row counts. Without it, leftover data from previous tests can cause false failures.

Test both happy paths and edge cases. An empty dataset, a dataset with the maximum allowed values, and a dataset with special characters are all worth testing. Don't only test the "normal" case.

DbUnit and Database Rider remove the main obstacle to database testing in Java: the difficulty of controlling state. With datasets defined in files and loaded automatically, you can write precise, isolated database tests as easily as any other unit test.

Read more

Start now free