@Rollback与@Transactional在Spring Boot验收测试中失效求助
问题:Spring Boot验收测试中数据库修改无法回滚
问题场景
使用Spring Boot Data JPA、JUnit5、H2数据库编写验收测试,测试可正常执行,但测试结束后数据库的创建、删除操作未回滚,数据被持久化保存。
现有代码分层
测试类
@ExtendWith(SpringExtension.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class ControllerAcceptanceTest { @LocalServerPort int randomServerPort; private RestTemplate restTemplate; private String url; @BeforeEach void setUp() { restTemplate = new RestTemplate(); } @Test @Transactional @Rollback void testCreate() throws Exception { url = "http://localhost:" + randomServerPort + "/api/wallet/create"; UUID uuid = UUID.randomUUID(); PlayerDto playerDto = new PlayerDto(); playerDto.setPlayerId(String.valueOf(uuid)); ResponseEntity responseEntity = restTemplate.postForEntity(url, playerDto, Object.class); assertEquals(HttpStatus.OK, responseEntity.getStatusCode()); } @Test @Transactional @Rollback void testDelete() throws Exception { url = "http://localhost:" + randomServerPort + "/api/wallet/delete"; WalletDto walletDto = new WalletDto(); walletDto.setWalletId("1"); ResponseEntity responseEntity = restTemplate.postForEntity(url, walletDto, Object.class); assertEquals(HttpStatus.OK, responseEntity.getStatusCode()); } }
Controller
@PostMapping (path = "/create", consumes = MediaType.APPLICATION_JSON_VALUE) public void createWallet(@RequestBody PlayerDto playerDto) throws PlayerIdRedundantException { walletService.createWallet(playerDto.getPlayerId()); } @PostMapping (path = "/delete", consumes = MediaType.APPLICATION_JSON_VALUE) public void deleteWallet(@RequestBody WalletDto walletDto) throws PlayerIdRedundantException { walletService.deleteWallet(walletDto.getWalletId()); }
Service
@Transactional public void createWallet(String playerId) throws PlayerIdRedundantException { checkPlayerId(playerId); Wallet wallet = Wallet.builder() .playerId(playerId) .balance(BigDecimal.valueOf(0)) .build(); walletRepository.save(wallet); } @Transactional public void deleteWallet(String walletId) throws PlayerIdRedundantException { Wallet wallet = walletRepository.findById(Long.valueOf(walletId)).orElseThrow(); walletRepository.delete(wallet); }
application.properties
spring.h2.console.enabled=true spring.datasource.url=jdbc:h2:file:./src/main/resources/data/testdb;AUTO_SERVER=TRUE;OLD_INFORMATION_SCHEMA=TRUE spring.datasource.username=sa spring.datasource.password= spring.jpa.database-platform=org.hibernate.dialect.H2Dialect spring.jpa.generate-ddl=true
原因分析
测试方法上的@Transactional和@Rollback仅作用于测试方法所在的线程,但通过RestTemplate发起的HTTP请求会创建一个独立的请求线程,该线程的事务由Service层的@Transactional管理,请求执行完成后事务直接提交,和测试方法的事务上下文完全隔离,因此测试结束后无法回滚这些请求产生的数据库修改。
另外,当前H2数据库使用文件模式,即使事务回滚,文件存储的数据也可能存在残留,不适合测试场景。
解决方案
方案1:改用TestRestTemplate(推荐)
TestRestTemplate是Spring提供的用于测试的Rest客户端,能和测试方法的事务上下文绑定,确保HTTP请求的操作在测试事务内执行,测试结束后统一回滚。
调整后的测试类:
@ExtendWith(SpringExtension.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class ControllerAcceptanceTest { @LocalServerPort int randomServerPort; @Autowired private TestRestTemplate testRestTemplate; private String url; @Test @Transactional @Rollback void testCreate() throws Exception { url = "http://localhost:" + randomServerPort + "/api/wallet/create"; UUID uuid = UUID.randomUUID(); PlayerDto playerDto = new PlayerDto(); playerDto.setPlayerId(String.valueOf(uuid)); ResponseEntity responseEntity = testRestTemplate.postForEntity(url, playerDto, Object.class); assertEquals(HttpStatus.OK, responseEntity.getStatusCode()); } @Test @Transactional @Rollback void testDelete() throws Exception { url = "http://localhost:" + randomServerPort + "/api/wallet/delete"; WalletDto walletDto = new WalletDto(); walletDto.setWalletId("1"); ResponseEntity responseEntity = testRestTemplate.postForEntity(url, walletDto, Object.class); assertEquals(HttpStatus.OK, responseEntity.getStatusCode()); } }
方案2:改用MockMvc(适合无服务器集成测试)
如果不需要启动完整的Web服务器,可使用MockMvc模拟请求,同样能和测试事务联动:
@ExtendWith(SpringExtension.class) @SpringBootTest @AutoConfigureMockMvc public class ControllerAcceptanceTest { @Autowired private MockMvc mockMvc; @Test @Transactional @Rollback void testCreate() throws Exception { UUID uuid = UUID.randomUUID(); PlayerDto playerDto = new PlayerDto(); playerDto.setPlayerId(String.valueOf(uuid)); mockMvc.perform(post("/api/wallet/create") .contentType(MediaType.APPLICATION_JSON) .content(new ObjectMapper().writeValueAsString(playerDto))) .andExpect(status().isOk()); } }
方案3:调整H2为内存模式+手动清理
如果坚持使用RestTemplate,需将H2改为内存模式避免持久化,并在测试后手动清理数据:
修改application.properties的数据库URL:
spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;OLD_INFORMATION_SCHEMA=TRUE
然后在测试类中添加@AfterEach方法清理数据:
@Autowired private WalletRepository walletRepository; @AfterEach void tearDown() { walletRepository.deleteAll(); }
内容的提问来源于stack exchange,提问作者Nikzin
相关产品推荐
相关产品推荐

