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
相关产品推荐
相关产品推荐

