
在Spring集成测试中,当使用@Transactional注解进行数据修改后,通过mockMvc模拟请求访问这些数据时,可能会遇到意外地读取到旧数据的问题。这通常是由于测试方法的主事务与mockMvc请求处理流程之间的事务隔离导致的。本文将深入探讨这一现象的原因,并提供使用TransactionTemplate进行显式事务管理的解决方案,确保测试中数据修改的即时可见性。
事务隔离与集成测试中的数据可见性
在spring集成测试中,当我们在测试方法上标注@transactional时,spring默认会在测试方法开始时启动一个事务,并在方法结束时回滚该事务,以保持数据库状态的清洁。这意味着在测试方法内部对数据库进行的所有修改,在事务提交之前,对于其他事务或不同的线程上下文是不可见的。
问题描述的场景正是这一机制的典型体现:
测试方法首先通过userRepository.findUserByUniqueName(“oldUniqueName”)获取一个用户。然后修改该用户的uniqueName为”newUniqueName”,并通过userRepository.saveAndFlush(user)保存。此时,这些修改是在当前测试方法的事务中进行的,尚未提交。接着,通过mockMvc模拟一个HTTP请求,请求头中包含”oldUniqueName”。在mockMvc请求处理过程中(例如,在安全过滤器中),尝试使用”oldUniqueName”再次查询用户。
由于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会话的混淆,但根本原因仍是事务边界不明确。
解决方案:使用TransactionTemplate显式管理事务
为了解决这个问题,我们需要确保在mockMvc请求被触发之前,主测试方法中对数据库的修改已经被提交。这可以通过移除测试方法上的@Transactional注解,并使用TransactionTemplate来手动管理事务边界来实现。TransactionTemplate允许我们在代码块内执行事务性操作,并可以明确地控制事务的提交或回滚。
先见AI
数据为基,先见未见
95 查看详情
步骤:
从测试方法上移除@Transactional注解。注入PlatformTransactionManager。使用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@AutoConfigureMockMvcclass 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); }); }}
代码解释:
@Test方法不再有@Transactional注解,这意味着Spring不会为整个测试方法默认启动一个事务并回滚。我们注入了PlatformTransactionManager,它是Spring事务管理的核心接口。通过new TransactionTemplate(transactionManager)创建了一个事务模板实例。executeWithoutResult(status -> { … })方法会启动一个新事务,执行Lambda表达式中的代码,并在代码块成功执行后提交事务。如果发生异常,事务会回滚。这样,在mockMvc.perform调用之前,userRepository.saveAndFlush(foundUser)所做的修改就已经被提交到数据库,对后续的mockMvc请求处理流程是可见的。当安全过滤器尝试查询”oldUniqueName”时,由于数据库中已不存在该名称的用户,它将正确地返回Optional.empty(),从而触发预期的异常和Unauthorized状态。
注意事项与总结
测试隔离: 尽管使用TransactionTemplate解决了数据可见性问题,但请记住,每次测试运行后,您可能需要手动清理数据库中由测试生成的数据,以确保测试之间的隔离性。在上述示例中,我们添加了一个额外的TransactionTemplate块来删除测试数据。事务边界: 深入理解Spring事务的传播行为和隔离级别对于编写健壮的集成测试至关重要。@Transactional注解在不同场景下的行为可能有所不同。缓存影响: 如果您的应用程序中存在二级缓存(如Ehcache, Redis等)或Hibernate一级缓存(Session缓存),它们也可能影响数据可见性。在某些情况下,除了事务提交,您可能还需要考虑刷新或清除相关缓存。然而,在本例中,主要问题是事务提交而非缓存。TestTransaction: Spring TestContext Framework 也提供了TestTransaction工具类,可以在@Transactional测试中手动控制事务(如TestTransaction.flagForCommit()),但这会改变@Transactional测试的默认回滚行为。对于本例,使用TransactionTemplate来精确控制特定代码块的事务提交更为直接和推荐。
通过上述方法,我们可以有效地解决Spring集成测试中因事务隔离导致的数据可见性问题,确保mockMvc模拟的请求能够正确地与数据库的最新状态进行交互,从而编写出更准确、更可靠的集成测试。
以上就是Spring集成测试中事务隔离与MockMvc的陷阱:旧数据为何依然可见?的详细内容,更多请关注创想鸟其它相关文章!
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/333127.html
微信扫一扫
支付宝扫一扫