
在spring boot的集成测试中,我们经常使用@transactional注解来确保每个测试方法都在一个独立的事务中运行,并在测试结束后自动回滚所有数据库操作,从而保持测试环境的清洁。然而,当测试流程涉及到mockmvc发起http请求时,这种默认的事务行为有时会引发预期之外的问题。
考虑一个典型的场景:
我们期望的是,由于用户名称已被修改,根据旧名称的查询应该返回空。但实际观察到的现象是,查询竟然成功找到了用户,并且返回的实体是已经更新过新名称的。这表明mockMvc请求内部的数据库查询,似乎看到了一个“旧数据”的视图,或者更准确地说,它看到了当前事务(测试方法)中尚未提交的更改,但又以一种混淆的方式呈现。
这个问题的核心在于事务的隔离性以及mockMvc请求的执行上下文。
当mockMvc请求在一个新事务中执行时,它将无法看到主测试方法事务中尚未提交的更改。因此,当安全过滤器尝试使用oldUniqueName查询时,它查询的是数据库中已提交的数据。如果主测试方法在mockMvc调用前没有提交其更改,那么数据库中仍然是oldUniqueName对应的记录(或者根本没有newUniqueName对应的已提交记录),这就会导致查询行为与预期不符。
为什么会看到“新名称”的实体? 这可能是因为在某些特定的事务隔离级别下,或者当mockMvc请求的事务与主测试事务共享了某个持久化上下文(如Hibernate Session)时,导致了这种混淆。但更常见且更可靠的解释是,mockMvc请求的事务未能看到主事务中未提交的更改。而实际观察到查询结果是“新名称”的实体,则暗示了某种更复杂的持久化上下文同步或缓存行为,使得旧名称的查询最终映射到了新名称的实体,这通常是由于Hibernate一级缓存或二级缓存与事务边界的交互导致的。
为了解决这个问题,我们需要确保在mockMvc请求执行之前,主测试方法中对数据库的更改能够被提交,从而对所有后续的独立事务可见。实现这一目标的方法是移除测试方法上的@Transactional注解(以避免整个测试方法事务回滚),并使用TransactionTemplate来手动管理需要提交的数据库操作。
TransactionTemplate是Spring提供的一种编程式事务管理方式,它允许我们定义一个事务的边界,并在其中执行数据库操作,然后明确地提交或回滚该事务。
修改后的测试代码示例:
首先,确保你的测试类中注入了PlatformTransactionManager,它是TransactionTemplate的构造函数参数。
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpHeaders;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest
@AutoConfigureMockMvc
// 移除 @Transactional 注解,以便我们可以手动控制事务提交
class UserIntegrationTest {
@Autowired
private UserRepository userRepository;
@Autowired
private MockMvc mockMvc;
@Autowired
private PlatformTransactionManager transactionManager; // 注入事务管理器
@Test
void testSecurityFilterWithChangedUser() throws Exception {
// 创建 TransactionTemplate 实例
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
final String oldUniqueName = "oldUniqueName";
final String newUniqueName = "newUniqueName";
final String endpointUrl = "/api/secure-endpoint"; // 假设的受保护接口
// 1. 初始化一个用户并保存,确保其在数据库中存在
transactionTemplate.execute(status -> {
User initialUser = new User();
initialUser.setUniqueName(oldUniqueName);
// 假设User实体有其他必要的字段,这里省略
userRepository.save(initialUser);
return null;
});
// 2. 在一个独立的事务中修改用户并提交
transactionTemplate.execute(status -> {
User user = userRepository.findUserByUniqueName(oldUniqueName)
.orElseThrow(() -> new IllegalStateException("User not found after initial save."));
assertThat(user).isNotNull();
user.setUniqueName(newUniqueName);
userRepository.saveAndFlush(user); // saveAndFlush 将更改同步到数据库
// TransactionTemplate 会在 execute 方法返回后自动提交事务
return null;
});
// 此时,数据库中已提交的用户记录的 uniqueName 应该是 "newUniqueName"
// 根据 oldUniqueName 查询应该返回 Optional.empty()
// 3. 构建 mockMvc 请求,使用旧的 uniqueName
HttpHeaders headers = new HttpHeaders();
headers.add("X-Unique-Name", oldUniqueName); // 假设安全过滤器从这个Header获取
// 4. 执行 mockMvc 请求,期望由于 oldUniqueName 不存在而导致未授权
mockMvc.perform(get(endpointUrl).headers(headers))
.andExpect(status().isUnauthorized()); // 期望未授权状态
// 5. (可选) 验证数据库状态,确保测试没有留下脏数据
// 可以再次使用 TransactionTemplate 清理或在 @AfterEach 中处理
transactionTemplate.execute(status -> {
userRepository.findUserByUniqueName(newUniqueName)
.ifPresent(userRepository::delete); // 清理测试数据
return null;
});
}
}安全过滤器示例(保持不变):
@Override
@Transactional // 过滤器内部通常也需要事务
protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String uniqueNameFromHeader = extractUniqueNameFromRequest(request);
try {
// 这里会查询数据库中已提交的数据
User user = userRepository.findUserByUniqueName(uniqueNameFromHeader)
.orElseThrow(() -> new Exception("User not found for header unique name"));
// update security context
filterChain.doFilter(request, response); // 继续请求链
}
catch(Exception e) {
// handle exception (e.g., set HTTP status to UNAUTHORIZED)
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
}
}在Spring Boot集成测试中,当@Transactional注解与mockMvc结合使用时,可能会遇到事务隔离导致的数据可见性问题。mockMvc请求通常会在一个独立于主测试方法的事务中执行,因此无法看到主事务中尚未提交的数据库更改。通过移除测试方法上的@Transactional注解,并使用TransactionTemplate来显式地管理和提交测试前置的数据库操作,可以确保在mockMvc请求发起时,数据库状态已经更新并对所有新事务可见,从而解决数据不一致的问题,使测试行为符合预期。理解事务边界和隔离级别对于编写健壮的集成测试至关重要。
以上就是Spring Boot集成测试中事务隔离与mockMvc的交互问题及解决方案的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号