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

Spring Boot集成测试中webEnvironment与Repository访问冲突问题

问题根源分析

你的问题核心在于事务边界和线程隔离,结合@Transactional测试注解的特性,导致测试线程和Web请求线程的数据库操作彼此不可见,具体拆解:

1. 事务与线程的独立性

你在测试类上加了@Transactional,Spring会为每个测试方法开启一个专属事务,且该事务与测试线程绑定。但你用Unirest发起的HTTP请求会被Spring Boot的Web容器分配到全新的线程处理,这个线程的事务上下文和测试线程完全隔离:

  • 场景A:Web请求的线程完成数据保存后,事务提交,数据已写入数据库,但测试线程的事务处于REPEATABLE_READ(MySQL默认隔离级别),只能看到事务启动时的数据库快照,无法读取其他事务后续提交的新数据。
  • 场景B:测试线程通过repository.save写入的数据,只会存在于测试事务的缓存中(尚未提交,因为@Transactional测试默认方法结束才回滚),Web请求的线程事务自然看不到未提交的数据。

2. EntityManager一级缓存的干扰

Spring Data JPA的Repository依赖EntityManager,而EntityManager的一级缓存是线程绑定的。测试线程的EntityManager缓存里没有Web请求写入的实体,即使数据库已有数据,直接调用findById可能优先查缓存,导致查不到;反过来Web线程的缓存也看不到测试线程的实体。


解决方案推荐

根据你的测试需求,按优先级推荐以下方案:

方案1:改用MockMvc模拟请求(最优)

把Web环境从DEFINED_PORT改为MOCK,用Spring提供的MockMvc模拟请求——这种方式不会启动真实的Web服务器,请求直接在测试线程中执行,和测试事务共享同一个EntityManager上下文:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK) // 修改这里
@ActiveProfiles("it")
@EnableAutoConfiguration(exclude = SecurityAutoConfiguration.class)
@Sql(scripts = {"classpath:schema_init.sql"})
@Transactional
public class ClientIntegrationTest {
    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private ClientRepository repository;

    // 场景A示例
    @Test
    @DisplayName("Create a new client")
    @Sql(scripts = "classpath:data_int.sql")
    void createNewClientTest() throws Exception {
        // given
        Client rawClient = new Client("testdetails");
        // when
        String responseJson = mockMvc.perform(post("myendpoint_save")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(rawClient)))
                .andExpect(status().isOk())
                .andReturn()
                .getResponse()
                .getContentAsString();
        
        Client savedClient = objectMapper.readValue(responseJson, Client.class);
        // then
        assertThat(repository.findById(savedClient.getId()).get())
                .isEqualTo(savedClient);
    }
}
  • 优势:保留@Transactional的自动回滚特性,无需手动清理测试数据;请求和测试操作完全共享事务上下文,不存在隔离问题。

方案2:移除测试类的@Transactional注解

如果必须用真实Web服务器(DEFINED_PORT),可以去掉测试类上的@Transactional:

  • 测试线程的操作会直接提交到数据库,Web请求的事务能看到;Web请求提交的数据,测试线程也能查询到。
  • 注意:需要手动清理测试数据,避免影响其他测试。可以用@Sql的executionPhase指定测试后执行清理脚本:
@Test
@DisplayName("Create a new client")
@Sql(scripts = "classpath:data_int.sql")
@Sql(scripts = "classpath:cleanup_client.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
void createNewClientTest() {
    // ... 原有测试代码 ...
}

方案3:手动控制事务提交/缓存刷新

针对特定场景临时调整:

  • 场景B:在调用Web接口前,手动提交测试事务的修改,让数据写入数据库:
@Autowired
private TransactionTemplate transactionTemplate;

@Test
void getClientByIdTest() {
    Client savedEntity = transactionTemplate.execute(status -> {
        Client client = repository.save(new Client("testdetails"));
        status.flush(); // 刷新到数据库
        status.commit(); // 提交事务
        return client;
    });
    // ... 调用Web接口并断言 ...
}
  • 场景A:在Web请求后,清除测试线程的EntityManager缓存,并调整事务隔离级别为READ_COMMITTED:
@Autowired
private EntityManager entityManager;

@Test
@Transactional(isolation = Isolation.READ_COMMITTED)
void createNewClientTest() {
    // ... Web请求代码 ...
    entityManager.clear(); // 清除一级缓存,强制从数据库查询
    assertThat(repository.findById(savedClient.getId()).get()).isEqualTo(savedClient);
}

内容的提问来源于stack exchange,提问作者Péter

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.07 17:47:26