SpringBoot集成测试MyBatis场景下如何实现单事务全链路操作并测试后回滚
问题核心原因
你遇到的问题本质是两个底层机制导致的:
- 手动创建的SqlSession和Spring托管的MyBatis Mapper、业务服务使用的连接不属于同一个事务上下文,未提交的插入数据对其他连接不可见,直接导致第一个断言失败
- 事务是和线程绑定的,
calculateService内部新开的线程无法继承主线程的事务,所有子线程的数据库操作都会开启独立事务,测试主线程的回滚操作完全覆盖不到这部分数据变更
解决方案
方案1:适配单线程执行,用Spring事务自动回滚(成本最低)
这个方案适合可以对业务代码做少量测试适配的场景,所有操作都在测试主线程的同一个事务中执行,测试结束自动回滚:
- 删掉自定义创建SqlSession、Spy注入、手动管理连接的逻辑,这部分逻辑会破坏Spring的事务托管机制
- 测试类添加
@Transactional和@Rollback(true)注解,Spring测试框架会自动为测试方法绑定全局事务,默认测试结束就会回滚所有操作 - 不要自己手动拿Connection执行SQL,注入Spring提供的
JdbcTemplate执行插入操作,JdbcTemplate会自动复用当前事务绑定的连接,插入的数据对同一个事务内的MyBatis Mapper查询可见 - 给
calculateService的方法添加测试环境开关,测试场景下不开启新线程,串行执行所有计算、存储逻辑,确保所有数据库操作都在测试主线程的事务内
修改后的代码示例:
@Slf4j @RunWith(SpringJUnit4ClassRunner.class) @SpringBootTest @Transactional @Rollback(true) public class MyTestClass { @Autowired private DataRepository dataRepository; @Autowired private CalculateService calculateService; @Autowired private JdbcTemplate jdbcTemplate; @Test public void myTest(){ // 用JdbcTemplate执行插入,复用当前事务的连接 String createdProductId = jdbcTemplate.queryForObject("Insert into ... RETURNING id", String.class); Product createdProduct = dataRepository.getProduct(createdProductId); assertNotNull(createdProduct); // 此处断言可以正常通过 // 测试场景下calculate方法不开新线程,所有操作都在当前事务 calculateService.calculate(createdProduct); createdProduct = dataRepository.getProduct(createdProductId); assertSomeField(createdProduct.getSomeField); } }
方案2:必须保留多线程逻辑的场景
如果不能修改业务代码的多线程逻辑,事务机制无法跨线程实现回滚,推荐以下两种方案:
子方案2.1 使用H2内存数据库
这是集成测试的主流方案,每次测试启动独立的内存数据库实例,测试结束后实例自动销毁,不需要手动处理数据回滚:
- 引入H2依赖,在测试配置文件
application-test.yml中配置内存数据源
spring: datasource: driver-class-name: org.h2.Driver url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1 username: sa password: sql: init: schema-locations: classpath:你的建表脚本路径.sql
- 测试类添加
@ActiveProfiles("test")启用测试配置即可。
子方案2.2 手动数据清理
如果必须使用测试环境的物理库,可以在测试前后手动清理关联表数据:
- 用
@Before方法在测试执行前清空本次测试涉及的所有业务表 - 用
@After方法在测试执行后再次清空关联表,避免测试数据残留
内容的提问来源于stack exchange,提问作者Fyodor Iz
相关产品推荐
相关产品推荐

