Spring Boot集成测试添加@Transactional后无数据返回问题
嘿,我之前也遇到过一模一样的问题,来给你把这个事儿说清楚,再给你几个可行的解决办法:
@Transactional就没数据了? 当你在测试类上加上@Transactional之后,Spring会给整个测试方法套一个独立的事务,而且默认会在测试结束后把这个事务回滚掉。重点来了:你用TestRestTemplate发的REST请求,是跑在Tomcat的单独线程里的,这个线程的事务和测试代码所在的事务完全是两个独立的上下文!
你在beforeEach里调用saveAll存的数据,都在测试的事务里待着——因为最后要回滚,所以这个事务根本没提交。而REST请求的线程看不到未提交的事务数据,自然返回空啦。
没加@Transactional的时候,saveAll的操作直接就提交到数据库了,REST接口当然能读到数据。
这里有几个靠谱的方案,你挑适合自己的来:
方案1:用@Sql注解提前插测试数据
@Sql可以在测试事务启动之前就执行SQL脚本把数据插进去,而且可以让它在独立的事务里执行,这样数据是提交状态,REST请求就能读到。同时测试类的@Transactional依然能保证测试过程中产生的数据被回滚,不会影响后续测试。
示例代码:
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @ActiveProfiles("int-test") @Transactional // 测试前插入数据,用独立事务执行 @Sql( scripts = ["classpath:sql/insert_test_organizations.sql"], executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, config = SqlConfig(transactionMode = SqlConfig.TransactionMode.ISOLATED) ) // 测试后清理数据,同样用独立事务 @Sql( scripts = ["classpath:sql/clean_test_organizations.sql"], executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD, config = SqlConfig(transactionMode = SqlConfig.TransactionMode.ISOLATED) ) class OrganizationControllerIntTest { // 这里就不用beforeEach存数据啦 @Test fun `When GET organizations then return all organizations`() { val result = restTemplate.getForEntity("/organizations", String::class.java) assertThat(result?.statusCode, equalTo(HttpStatus.OK)) assertThat(result?.body, containsString("items")) } }
方案2:用TransactionTemplate开独立事务存数据
如果你不想写SQL脚本,想用代码生成测试数据,可以用TransactionTemplate开一个新的事务来存数据,确保这个事务提交,这样REST请求就能读到。测试结束后,测试类的@Transactional会回滚测试过程的修改,不过你得手动清理提前存的测试数据。
示例代码:
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @ActiveProfiles("int-test") @Transactional class OrganizationControllerIntTest { @Autowired private lateinit var organizationRepository: OrganizationRepository @Autowired private lateinit var restTemplate: TestRestTemplate @Autowired private lateinit var testDataGenerator: TestDataGenerator @Autowired private lateinit var transactionTemplate: TransactionTemplate @BeforeEach fun beforeEach() { // 开一个新的事务,存完数据就提交 transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW) transactionTemplate.execute { val organizations = testDataGenerator.organizations(10).toList() organizationRepository.saveAll(organizations) null } } @AfterEach fun afterEach() { // 同样开新事务清理数据,避免影响其他测试 transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW) transactionTemplate.execute { organizationRepository.deleteAll() null } } @Test fun `When GET organizations then return all organizations`() { val result = restTemplate.getForEntity("/organizations", String::class.java) assertThat(result?.statusCode, equalTo(HttpStatus.OK)) assertThat(result?.body, containsString("items")) } }
方案3:去掉@Transactional,手动清理数据
如果你的测试不需要回滚事务,干脆把测试类上的@Transactional去掉,在@AfterEach里手动删掉测试数据就行。这种方式简单直接,就是每次测试后要自己清理,不然数据会残留。
示例代码:
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @ActiveProfiles("int-test") class OrganizationControllerIntTest { @Autowired private lateinit var organizationRepository: OrganizationRepository @Autowired private lateinit var restTemplate: TestRestTemplate @Autowired private lateinit var testDataGenerator: TestDataGenerator @BeforeEach fun beforeEach() { val organizations = testDataGenerator.organizations(10).toList() organizationRepository.saveAll(organizations) } @AfterEach fun afterEach() { organizationRepository.deleteAll() } @Test fun `When GET organizations then return all organizations`() { val result = restTemplate.getForEntity("/organizations", String::class.java) assertThat(result?.statusCode, equalTo(HttpStatus.OK)) assertThat(result?.body, containsString("items")) } }
核心就是测试线程的事务和REST请求线程的事务是隔离的,只要让测试数据处于已提交的状态,REST接口就能读到。你可以根据自己的需求选方案,比如想保留测试回滚就用前两个,想简单直接就用第三个。
内容的提问来源于stack exchange,提问作者pgiecek

