在使用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(databaseConnection = "deliveryDataSource")
由此可见,不熟悉dbunit原理,使用其进行单元测试,也是有一定风险的。
附录:
在写单测时,除了需要写数据库单测,还需要写上层业务的单测,推荐mockito + dbunit结合。常规依赖如下:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.3.19</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test</artifactId>
<scope>test</scope>
<version>2.6.7</version>
<exclusions>
<exclusion>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-to-slf4j</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.dbunit</groupId>
<artifactId>dbunit</artifactId>
<version>2.7.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.springtestdbunit</groupId>
<artifactId>spring-test-dbunit</artifactId>
<version>1.3.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.5.1</version>
<scope>test</scope>
</dependency>
</dependencies>
上层业务的基类定义如下:
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* Create by yujing10 on 2020/11/18.
*/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = StartAppTest.class)
@ActiveProfiles("test")
public class SpringTestBase {
}
StartAppTest类如下:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration;
import org.springframework.boot.autoconfigure.elasticsearch.rest.RestClientAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.context.annotation.ImportResource;
@SpringBootApplication(exclude = {
RestClientAutoConfiguration.class,
RabbitAutoConfiguration.class,
DataSourceAutoConfiguration.class
})
@ImportResource("classpath:/applicationContext.xml")
public class StartAppTest {
public static void main(String[] args) {
SpringApplication.run(StartAppTest.class, args);
}
}
dbunit的基类:
import com.github.springtestdbunit.TransactionDbUnitTestExecutionListener;
import com.github.springtestdbunit.annotation.DbUnitConfiguration;
import org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import org.springframework.test.context.support.DirtiesContextTestExecutionListener;
@TestExecutionListeners({
DependencyInjectionTestExecutionListener.class,
DirtiesContextTestExecutionListener.class,
TransactionDbUnitTestExecutionListener.class,
MockitoTestExecutionListener.class
})
@DbUnitConfiguration(databaseConnection = "deliveryDataSource")
public class DbTestBase extends SpringTestBase{
}
1、DBUnit的原理_漢家郎的博客-CSDN博客 DBUnit的原理
2、https://my.oschina.net/linuxred/blog/61221 DbUnit中的DatabaseOperation介绍