You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

@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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 00:40:14