1. 概述

本文将介绍 DBUnit —— 一个用于 Java 应用中数据库交互测试 的单元测试工具。

它的核心价值在于:帮助我们在测试前将数据库置于一个已知的、可预测的状态,并在测试后验证数据库是否达到了预期状态。这在测试 DAO 层、Repository 层时尤其有用,避免了测试之间因数据污染导致的不稳定问题。

简单说,它让你的数据库测试也能做到“干净输入,明确输出”,而不是靠猜。

2. 依赖配置

使用 DBUnit 非常简单,只需在 pom.xml 中引入对应依赖即可:

<dependency>
  <groupId>org.dbunit</groupId>
  <artifactId>dbunit</artifactId>
  <version>2.7.0</version>
  <scope>test</scope>
</dependency>

建议前往 Maven Central 查看最新版本,避免踩坑旧版本的兼容性问题。

3. 快速上手示例

我们先从一个简单的例子开始,快速感受 DBUnit 的工作流程。

3.1. 定义数据库 Schema

首先准备一个建表脚本,用于初始化内存数据库结构:

schema.sql:

CREATE TABLE IF NOT EXISTS CLIENTS
(
    `id`         int AUTO_INCREMENT NOT NULL,
    `first_name` varchar(100)       NOT NULL,
    `last_name`  varchar(100)       NOT NULL,
    PRIMARY KEY (`id`)
);

CREATE TABLE IF NOT EXISTS ITEMS
(
    `id`       int AUTO_INCREMENT NOT NULL,
    `title`    varchar(100)       NOT NULL,
    `produced` date,
    `price`    float,
    PRIMARY KEY (`id`)
);

3.2. 定义初始测试数据集

DBUnit 的优势之一是支持声明式地定义测试数据。你可以用 XML、CSV、Excel 等格式来描述数据,这里我们使用 XML。

data.xml:

<?xml version="1.0" encoding="UTF-8"?>
<dataset>
    <CLIENTS id='1' first_name='Charles' last_name='Xavier'/>
    <ITEMS id='1' title='Grey T-Shirt' price='17.99' produced='2019-03-20'/>
    <ITEMS id='2' title='Fitted Hat' price='29.99' produced='2019-03-21'/>
    <ITEMS id='3' title='Backpack' price='54.99' produced='2019-03-22'/>
    <ITEMS id='4' title='Earrings' price='14.99' produced='2019-03-23'/>
    <ITEMS id='5' title='Socks' price='9.99'/>
</dataset>

✅ 每个 XML 元素代表一行数据,标签名是表名,属性名对应列名,清晰直观。

3.3. 初始化数据库连接与测试环境

接下来,我们需要继承 DataSourceBasedDBTestCase,并实现两个核心方法:

  • getDataSource():提供数据库连接
  • getDataSet():加载测试数据集

DataSourceDBUnitTest.java:

public class DataSourceDBUnitTest extends DataSourceBasedDBTestCase {
    @Override
    protected DataSource getDataSource() {
        JdbcDataSource dataSource = new JdbcDataSource();
        dataSource.setURL(
          "jdbc:h2:mem:default;MODE=LEGACY;DB_CLOSE_DELAY=-1;init=runscript from 
             'classpath:schema.sql'");
        dataSource.setUser("sa");
        dataSource.setPassword("sa");
        return dataSource;
    }

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

⚠️ 注意:

  • 使用了 H2 内存数据库,通过 init=runscript 自动执行建表脚本。
  • DB_CLOSE_DELAY=-1 确保数据库在 JVM 退出前不关闭。

数据库操作策略配置

DBUnit 默认在每个测试方法执行前重置数据,行为由以下两个方法控制:

@Override
protected DatabaseOperation getSetUpOperation() {
    return DatabaseOperation.REFRESH;
}

@Override
protected DatabaseOperation getTearDownOperation() {
    return DatabaseOperation.DELETE_ALL;
}
操作 说明
REFRESH 初始化时清空表并插入 XML 中定义的数据,保证测试独立
DELETE_ALL 测试结束后删除所有数据,避免污染

✅ 推荐使用 REFRESH + DELETE_ALL 组合,简单粗暴且可靠。

3.4. 验证数据库状态

现在可以写第一个测试了:验证初始数据是否正确加载。

@Test
public void givenDataSetEmptySchema_whenDataSetCreated_thenTablesAreEqual() throws Exception {
    IDataSet expectedDataSet = getDataSet();
    ITable expectedTable = expectedDataSet.getTable("CLIENTS");
    IDataSet databaseDataSet = getConnection().createDataSet();
    ITable actualTable = databaseDataSet.getTable("CLIENTS");
    assertEquals(expectedTable, actualTable);
}

这个测试会:

  1. 读取 XML 中的 CLIENTS 表数据
  2. 查询数据库中实际的 CLIENTS 表内容
  3. 断言两者是否一致

如果一切正常,测试通过 ✅

4. 深入 DBUnit 断言机制

基础的表对比只是开始,DBUnit 提供了更灵活的断言方式,应对复杂场景。

4.1. 使用 SQL 查询进行断言

有时候你只想验证查询结果,而不是整张表。这时可以用 createQueryTable()

@Test
public void givenDataSet_whenInsert_thenTableHasNewClient() throws Exception {
    try (InputStream is = getClass().getClassLoader().getResourceAsStream("dbunit/expected-user.xml")) {
        IDataSet expectedDataSet = new FlatXmlDataSetBuilder().build(is);
        ITable expectedTable = expectedDataSet.getTable("CLIENTS");
        Connection conn = getDataSource().getConnection();

        conn.createStatement()
            .executeUpdate(
            "INSERT INTO CLIENTS (first_name, last_name) VALUES ('John', 'Jansen')");
        ITable actualData = getConnection()
            .createQueryTable(
                "result_name",
                "SELECT * FROM CLIENTS WHERE last_name='Jansen'");

        assertEqualsIgnoreCols(expectedTable, actualData, new String[] { "id" });
    }
}

✅ 关键点:

  • createQueryTable() 接收一个 SQL 查询,返回结果封装为 ITable
  • 你可以只验证特定条件下的数据,灵活性更高
  • 使用 assertEqualsIgnoreCols 忽略 id 这类自增字段

4.2. 忽略特定列进行比较

数据库中常有无法预知的字段,比如:

  • 自增主键(id
  • 创建时间(created_at
  • 时间戳(updated_at

直接比较会失败,但 DBUnit 提供了优雅的解决方案:列过滤

@Test
public void givenDataSet_whenInsert_thenGetResultsAreStillEqualIfIgnoringColumnsWithDifferentProduced()
  throws Exception {
    Connection connection = tester.getConnection().getConnection();
    String[] excludedColumns = { "id", "produced" };
    try (InputStream is = getClass().getClassLoader()
      .getResourceAsStream("dbunit/expected-ignoring-registered_at.xml")) {
        IDataSet expectedDataSet = new FlatXmlDataSetBuilder().build(is);
        ITable expectedTable = excludedColumnsTable(expectedDataSet.getTable("ITEMS"), excludedColumns);

        connection.createStatement()
          .executeUpdate("INSERT INTO ITEMS (title, price, produced)  VALUES('Necklace', 199.99, now())");

        IDataSet databaseDataSet = tester.getConnection().createDataSet();
        ITable actualTable = excludedColumnsTable(databaseDataSet.getTable("ITEMS"), excludedColumns);

        assertEquals(expectedTable, actualTable);
    }
}

辅助方法示例:

private ITable excludedColumnsTable(ITable table, String[] excludedColumns) {
    return DefaultColumnFilter.excludedColumnsTable(table, excludedColumns);
}

✅ 使用 DefaultColumnFilter.excludedColumnsTable() 可以轻松排除指定列,避免断言失败。

4.3. 收集多个断言失败信息

默认情况下,DBUnit 遇到第一个不匹配就会抛出 AssertionError,不利于排查问题。

我们可以使用 DiffCollectingFailureHandler收集所有差异,一次性输出:

@Test
public void givenDataSet_whenInsertUnexpectedData_thenFailOnAllUnexpectedValues() throws Exception {
    try (InputStream is = getClass().getClassLoader()
      .getResourceAsStream("dbunit/expected-multiple-failures.xml")) {
        IDataSet expectedDataSet = new FlatXmlDataSetBuilder().build(is);
        ITable expectedTable = expectedDataSet.getTable("ITEMS");
        Connection conn = getDataSource().getConnection();
        DiffCollectingFailureHandler collectingHandler = new DiffCollectingFailureHandler();

        conn.createStatement()
          .executeUpdate("INSERT INTO ITEMS (title, price) VALUES ('Battery', '1000000')");
        ITable actualData = getConnection().createDataSet().getTable("ITEMS");

        assertEquals(expectedTable, actualData, collectingHandler);
        if (!collectingHandler.getDiffList().isEmpty()) {
            String message = (String) collectingHandler.getDiffList()
                .stream()
                .map(d -> formatDifference((Difference) d))
                .collect(joining("\n"));
            logger.error(() -> message);
        }
    }
}

private static String formatDifference(Difference diff) {
    return "expected value in " + diff.getExpectedTable()
      .getTableMetaData()
      .getTableName() + "." + 
      diff.getColumnName() + " row " + 
      diff.getRowIndex() + ":" + 
      diff.getExpectedValue() + ", but was: " + 
      diff.getActualValue();
}

运行后输出:

java.lang.AssertionError: expected value in ITEMS.price row 5:199.99, but was: 1000000.0
expected value in ITEMS.produced row 5:2019-03-23, but was: null
expected value in ITEMS.title row 5:Necklace, but was: Battery

✅ 优势:

  • 一次看到所有字段差异,不用反复调试
  • 可自定义错误格式,便于集成到 CI/CD 日志中

5. 总结

DBUnit 为 Java 应用的数据库测试提供了一套成熟、可靠的解决方案:

  • 声明式数据定义:通过 XML/CSV 轻松管理测试数据
  • 测试隔离:自动重置数据库状态,避免测试间相互干扰
  • 灵活断言:支持 SQL 查询、列过滤、多差异常收集
  • 兼容主流数据库:H2、MySQL、PostgreSQL 等均可使用

虽然现代 Spring Test + @Sql 注解也能实现类似功能,但在复杂数据场景下,DBUnit 依然更具灵活性和可控性。

所有示例代码已托管至 GitHub:https://github.com/baeldung/tutorials(路径:libraries-testing


原始标题:Introduction to DBUnit