zl程序教程

您现在的位置是:首页 >  Python

当前栏目

dbunit实现原理及最佳实践

2023-03-15 22:01:13 时间

在使用dbunit写单元测试时,强烈建议先熟悉其底层的实现原理,否则可能导致数据表中的数据被清空的风险(尽管测试数据不如线上数据重要,但如果大量的测试数据被清空,导致测试环境不可用,数据恢复起来还是很头疼的!!!)。参考博客1介绍了dbunit的实现原理,但博主对此说法并不认同。不认同的点有以下两点:第一,参考博客1中说dbunit实现事务的方式是在测试前把数据库里的数据以XML的格式导出来,测试结束之后再将xml格式的数据导入数据库;第二,在运行每一个测试之前先把当前数据库里的数据清空。

我们先解释第二点。实际上,运行测试时初始数据的初始化策略是支持配置的。@DatabaseSetup注解有一个type属性,其作用是指定初始化测试数据的方式,取值范围如下:

public enum DatabaseOperation {
	UPDATE,
	INSERT,
	REFRESH,
	DELETE,
	DELETE_ALL,
	TRUNCATE_TABLE,
	CLEAN_INSERT;
}

其默认值为CLEAN_INSERT,含义是先将数据表清空,然后再将需要初始化的数据插入数据表中。然而,以其中的REFRESH为例,当type值为REFRESH时,就不再是清空数据表,而是采用更新的方式,即对于不存在的数据则插入,已存在的数据则更新字段。由此可见,第二条肯定是错的。

我们再来看第一点,分析如下:利用数据库的事务功能,dbunit完全可以在单测前开启事务,在单测结束后回滚事务即可,何须将数据记录先导出再导入呢?况且,假如真的是先导出再导入的化,如果单测前数据表中的数据记录特别多的化,导出导入过程将非常耗时。你可能会说,如果数据库本身不支持事务(比如MyISAM引擎)该怎么办呢?其实如果数据库不支持事务的化,当前面说的type值为REFRESH时,由dbunit来实现事务将会非常复杂,因为这意味着dbunit不仅仅需要考虑单测前数据表中的数据,还得考虑单测过程中对数据表所做的修改。由此可见,dbunit没有自己实现事务的理由。

到此,本文给出默认配置(type值为CLEAN_INSERT)时的实现过程如下:

第一步:如果单测配置了事务则开启事务;否则没有第一步;
第二步:运行每一个测试之前先把当前数据表里的数据清空;
第三步:将@DatabaseSetup注解对应的数据初始化到数据表中;
第四步:执行单测里的数据表操作;
第五步:将数据表中的所有数据查出来,和@ExpectedDatabase注解中的数据进行匹配验证;
第六步:如果单测配置了事务则回滚事务,数据表回到单测前状态;否则没有第六步。

由此可见,在单测的第二步中,会将数据表里的数据清空。所以如果你的dbunit单测没有加事务的化(@Transactional注解),数据就有被清空的风险。使用默认配置的推荐方式:

    @Transactional//加上@Transactional注解才会在单测结束之后回滚事务
    //@DatabaseSetup中的value为需要初始化到数据表中的数据,type表示初始化方式,默认值就是CLEAN_INSERT,表示先将数据表清空,再将xml中的数据插入
    @DatabaseSetup(value="test_setup.xml", type = DatabaseOperation.CLEAN_INSERT)
    //执行完数据表操作后,将数据表中的所有数据查询出来和xml中的数据进行比较
    @ExpectedDatabase(value = "test_expect.xml")
    @Test
    public void test() {
        //执行数据表操作,此处略
    }

当数据表中已经有较多数据时,建议采用更新数据表的方式处理老数据的配置方案:

    @Transactional//加上@Transactional注解才会在单测结束之后回滚事务
    //将@DatabaseSetup注解的属性type改成refresh,表示不会将原数据清空,而是将数据表中存在的xml中的数据进行更新,不存在的则进行插入
    @DatabaseSetup(value="test_setup.xml", type = DatabaseOperation.REFRESH)
    //执行完数据表操作后,将执行query中的sql,从数据表中查询出数据,并和xml中的数据进行比较
    @ExpectedDatabase(value = "test_expect.xml", table = "table_name", query = "select * from table_name where ...")
    @Test
    public void test() {
        //执行数据表操作,此处略
    }

当我们将DatabaseSetup的操作方式(type)改成refresh后,我们必须要在@ExpectedDatabase中指定table和query,因为refresh没有清空数据表中的数据,如果不通过query限定查询范围,则返回的是数据表中的全量数据,显然会导致验证不通过。

当DatabaseSetup的操作方式(type)改成refresh后dbunit的执行过程可以总结如下:

第一步:如果单测配置了事务则开启事务;否则没有第一步;
第二步:将@DatabaseSetup注解对应的数据初始化到数据表中,已存在的数据字段被更新,不存在的数据则插入;
第三步:执行单测里的数据表操作;
第四步:执行sql语句将数据表中对应的数据查出来,和@ExpectedDatabase注解中的数据进行匹配验证;
第五步:如果单测配置了事务则回滚事务,数据表回到单测前状态;否则没有第六步。

最后,为了让dbunit的@Transactional和@DatabaseSetup等注解能生效,还需要以下注解:

@TestExecutionListeners({
		DependencyInjectionTestExecutionListener.class,
		DirtiesContextTestExecutionListener.class,
		TransactionDbUnitTestExecutionListener.class,
		MockitoTestExecutionListener.class
})
@DbUnitConfiguration

由此可见,不熟悉dbunit原理,使用其进行单元测试,也是有一定风险的。

1、https://blog.csdn.net/jungle0127/article/details/7299260  DBUnit的原理

2、https://my.oschina.net/linuxred/blog/61221 DbUnit中的DatabaseOperation介绍