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);
}
这个测试会:
- 读取 XML 中的
CLIENTS
表数据 - 查询数据库中实际的
CLIENTS
表内容 - 断言两者是否一致
如果一切正常,测试通过 ✅
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
)