
在spring集成测试中,当我们在测试方法上标注@transactional时,spring默认会在测试方法开始时启动一个事务,并在方法结束时回滚该事务,以保持数据库状态的清洁。这意味着在测试方法内部对数据库进行的所有修改,在事务提交之前,对于其他事务或不同的线程上下文是不可见的。
问题描述的场景正是这一机制的典型体现:
由于mockMvc模拟的请求通常会在其自身的线程或事务上下文中执行,这个上下文并不能“看到”主测试方法中尚未提交的事务修改。因此,当安全过滤器尝试查询"oldUniqueName"时,它看到的是修改前的数据状态,或者更准确地说,它无法看到主测试事务中对该记录的更新,从而导致查询结果不符合预期。它可能查询到的是一个在它自己的事务视角下,仍然具有"oldUniqueName"的用户,或者甚至由于某种缓存机制,错误地返回了更新后的实体但其状态并未正确同步。
@Test
@Transactional // 这里的事务导致了问题
void test() {
User user = userRepository.findUserByUniqueName("oldUniqueName").orElse(null);
assertThat(user).isNotNull();
user.setUniqueName("newUniqueName");
userRepository.saveAndFlush(user); // 修改在此事务中,未提交
// headers中添加oldUniqueName
HttpHeaders headers = new HttpHeaders();
headers.add("Unique-Name", "oldUniqueName");
mockMvc
.perform(get("/api/user").headers(headers)) // mockMvc请求
.andExpect(status().isUnauthorized()); // 预期未授权,因为找不到oldUniqueName的用户
}
// 安全过滤器中的查询逻辑
@Override
@Transactional // 过滤器可能也有自己的事务
protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String oldUniqueName = extractUniqueNameFromRequest(request);
try {
// 这里的查询是在mockMvc请求的上下文中执行的,
// 它看不到上面测试方法中未提交的修改
User user = userRepository.findUserByUniqueName(oldUniqueName).orElseThrow(new Exception());
// ... 更新安全上下文
}
catch(Exception e) {
// ... 处理异常
}
}在这个场景中,mockMvc内部的userRepository.findUserByUniqueName(oldUniqueName)查询,由于主测试事务未提交,将无法找到"oldUniqueName"的用户(因为该用户已被更新),或者更糟糕的是,它可能会从缓存中获取到一个旧状态的User对象,或者从数据库中看到一个旧记录。而实际观察到的现象是,它找到了一个用户,但其uniqueName却是"newUniqueName",这暗示着某种缓存或者ORM会话的混淆,但根本原因仍是事务边界不明确。
为了解决这个问题,我们需要确保在mockMvc请求被触发之前,主测试方法中对数据库的修改已经被提交。这可以通过移除测试方法上的@Transactional注解,并使用TransactionTemplate来手动管理事务边界来实现。TransactionTemplate允许我们在代码块内执行事务性操作,并可以明确地控制事务的提交或回滚。
步骤:
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.test.web.servlet.MockMvc;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.http.HttpHeaders;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
@AutoConfigureMockMvc
class UserIntegrationTest {
@Autowired
private UserRepository userRepository;
@Autowired
private MockMvc mockMvc;
@Autowired
private PlatformTransactionManager transactionManager; // 注入事务管理器
@Test
void testUserUpdateVisibility() throws Exception {
// 使用TransactionTemplate确保此块内的操作被提交
new TransactionTemplate(transactionManager).executeWithoutResult(status -> {
User user = new User("oldUniqueName"); // 假设User有一个构造函数
userRepository.save(user); // 保存初始用户
User foundUser = userRepository.findUserByUniqueName("oldUniqueName").orElse(null);
assertThat(foundUser).isNotNull();
foundUser.setUniqueName("newUniqueName");
userRepository.saveAndFlush(foundUser); // 更新用户并刷新
});
// 至此,对数据库的修改(oldUniqueName -> newUniqueName)已提交
// 模拟mockMvc请求,此时数据库中已无"oldUniqueName"的用户
HttpHeaders headers = new HttpHeaders();
headers.add("Unique-Name", "oldUniqueName");
mockMvc
.perform(get("/api/user").headers(headers))
.andExpect(status().isUnauthorized()); // 预期未授权,因为找不到oldUniqueName的用户
// 可选:在测试结束时清理数据,以保持数据库状态清洁
new TransactionTemplate(transactionManager).executeWithoutResult(status -> {
userRepository.findUserByUniqueName("newUniqueName").ifPresent(userRepository::delete);
});
}
}代码解释:
通过上述方法,我们可以有效地解决Spring集成测试中因事务隔离导致的数据可见性问题,确保mockMvc模拟的请求能够正确地与数据库的最新状态进行交互,从而编写出更准确、更可靠的集成测试。
以上就是Spring集成测试中事务隔离与MockMvc的陷阱:旧数据为何依然可见?的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号