前言
我们在写单元测试时,有一个比较重要的要求是可以重复运行
,即只要外部参数不变
,那么一定可以获取确定的结果
。 那么这样就会有一个比较麻烦的问题:数据污染
。 以数据库操作为例,对于一些查询类的测试用例倒还好,因为只要保证数据存在,那么查询操作天生就是幂等的,不管你查多少次,数据都不会变。 但是对于写入操作就不友好了,每一次运行单元测试,都会在数据库中产生新的数据,可能在第一次运行时正常,第二次运行时就由于数据冲突导致失败。那对于这种情况,我们应该如何去解决?
下面分享两种我使用过的数据隔离的方式,供大家参考。
数据隔离
测试事务
这种方式比较常规,适用场景也比较广泛,即我们在测试用例中,通过@Transactional
注解开始事务,此时整个单元测试方法会被包裹在事务中,并且会在运行结束后,自动回滚。
我们可以在类或方法上添加@Transactional
注解
/**
* 在类上添加 @Transactional 注解,可以让测试方法在执行完毕后自动回滚,不会对数据库造成影响。
* 如果在方法上添加,则只影响对应的方法
*/
@SpringBootTest
@Transactional
public class TransactionalTests extends AbstractTransactionalJUnit4SpringContextTests {
@Resource
private UserService userService;
@Test
void createRecord() {
User user = userService.createUser();
//to other things
}
}
或者继承AbstractTransactionalJUnit4SpringContextTests
类
/**
* 在类上添加 @Transactional 注解,可以让测试方法在执行完毕后自动回滚,不会对数据库造成影响。
* 如果在方法上添加,则只影响对应的方法
*/
@SpringBootTest
public class TransactionalTests extends AbstractTransactionalJUnit4SpringContextTests {
@Resource
private UserService userService;
@Test
void createRecord() {
User user = userService.createUser();
//to other things
}
}
AbstractTransactionalJUnit4SpringContextTests
实际上也是添加了@Transactional
注解,只不过在此之外,它还提供了一些JDBC接口
,可以让我们更方便的操作数据库。
使用这种方式来实现单元测试的话,由于测试用例中的所有数据库操作都在事务中进行,然后运行结束后事务回滚,就可以保证不会染污数据库数据,做到数据隔离。但是它也存在一些问题:
-
影响单元测试结果,在方法上层添加事务,本质上还是改变了方法的逻辑。比如说我被测试的方法中用到了不同的数据源(主从数据库),一但被包裹了事务,那么意味着我在这个事务内,都是使用同一条连接,这会对我们实际上期望运行的逻辑生产影响。
-
排查问题困难,因为在单元测试运行结束之后,事务最终会回滚,也就意味着我们DB最终是没有数据的,很难排查问题。
数据预处理与清理
上面说到事务会存在一些问题,那么我们不使用事务,怎么保证数据隔离?我们可以通过几种方式来在单元测试运行前后手动的增加或清理数据:
通过@Sql
注解来定义单元测试前后需要执行的SQL脚本
@Sql(
scripts = "create-data.sql",
config = @SqlConfig(transactionMode = ISOLATED)
)
@Sql(
scripts = "delete-data.sql",
config = @SqlConfig(transactionMode = ISOLATED),
executionPhase = AFTER_TEST_METHOD
)
@Test
void createRecord() {
User user = userService.createUser();
//to other things
}
我们通过@Sql
注解定义指定了两个SQL脚本,分别用于在运行单元测试前创建测试数据,以及在运行结束后,删除数据,这样就可以保证我们的每次运行单元测试时的数据都是隔离的。
@Sql只是一种方式,我们还可以通过其它各种方式来执行,比如@BeforeEach、@AfterEach来定义单元测试执行前后的拦截器,或者Junit的@ExtendWith来定义外部的监听器等各种方式来处理数据的预处理与清理。
总结
上面分享了两种数据隔离的方式,一般情况下都可以满足我们的单元测试需求,但是实际上两种方式都还存在一些问题,比如不管是哪种方式,都依赖于外部的数据库
,如果我们依赖的数据库出现了异常,也会影响到我们的单元测试运行。而且使用外部数据库也是无法完全做到数据隔离的,还是会有数据冲突的风险
。那还有没有更好的方法? 如果大家的单元测试环境有docker环境的话,那么可以考虑引入Testcontainers
,可以很好的解决这个问题。下篇文章我会详细讲一下Testcontainers
在单测数据隔离中的实践。