当前位置: 首页 > 工具软件 > DbUnit > 使用案例 >

dbunit实现原理及最佳实践

伏子辰
2023-12-01

在使用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介绍

 类似资料: