
在spring boot的集成测试中,我们经常使用@transactional注解来确保每个测试方法都在一个独立的事务中运行,并在测试结束后自动回滚所有数据库操作,从而保持测试环境的清洁。然而,当测试流程涉及到mockmvc发起http请求时,这种默认的事务行为有时会引发预期之外的问题。
考虑一个典型的场景:
- 集成测试方法修改了一个实体(例如,更新用户的唯一名称)。
- 测试方法调用userRepository.saveAndFlush()保存更改。
- 随后,mockMvc发起一个HTTP请求,该请求会触发一个安全过滤器,并在过滤器中尝试根据旧的唯一名称查询用户。
我们期望的是,由于用户名称已被修改,根据旧名称的查询应该返回空。但实际观察到的现象是,查询竟然成功找到了用户,并且返回的实体是已经更新过新名称的。这表明mockMvc请求内部的数据库查询,似乎看到了一个“旧数据”的视图,或者更准确地说,它看到了当前事务(测试方法)中尚未提交的更改,但又以一种混淆的方式呈现。
2. 问题根源:@Transactional与mockMvc的事务边界这个问题的核心在于事务的隔离性以及mockMvc请求的执行上下文。
- @Transactional在测试方法上: 当一个测试方法被@Transactional注解时,Spring会为该方法创建一个事务。所有在该方法内部对数据库的操作(包括userRepository.saveAndFlush())都会在这个事务中进行。saveAndFlush()会确保更改被刷新到数据库会话中,但这些更改在事务提交之前对其他事务是不可见的(或者根据隔离级别可能部分可见)。在测试结束时,这个事务通常会被回滚。
- mockMvc请求的执行: mockMvc发起的HTTP请求通常会在一个独立的线程中执行其内部逻辑,包括调用控制器、服务层以及安全过滤器。如果这个内部逻辑也涉及到数据库操作(例如,安全过滤器中的userRepository.findUserByUniqueName),那么这些操作可能会在一个与测试方法主事务不同的新事务中执行。
当mockMvc请求在一个新事务中执行时,它将无法看到主测试方法事务中尚未提交的更改。因此,当安全过滤器尝试使用oldUniqueName查询时,它查询的是数据库中已提交的数据。如果主测试方法在mockMvc调用前没有提交其更改,那么数据库中仍然是oldUniqueName对应的记录(或者根本没有newUniqueName对应的已提交记录),这就会导致查询行为与预期不符。
为什么会看到“新名称”的实体? 这可能是因为在某些特定的事务隔离级别下,或者当mockMvc请求的事务与主测试事务共享了某个持久化上下文(如Hibernate Session)时,导致了这种混淆。但更常见且更可靠的解释是,mockMvc请求的事务未能看到主事务中未提交的更改。而实际观察到查询结果是“新名称”的实体,则暗示了某种更复杂的持久化上下文同步或缓存行为,使得旧名称的查询最终映射到了新名称的实体,这通常是由于Hibernate一级缓存或二级缓存与事务边界的交互导致的。
3. 解决方案:使用TransactionTemplate显式管理事务为了解决这个问题,我们需要确保在mockMvc请求执行之前,主测试方法中对数据库的更改能够被提交,从而对所有后续的独立事务可见。实现这一目标的方法是移除测试方法上的@Transactional注解(以避免整个测试方法事务回滚),并使用TransactionTemplate来手动管理需要提交的数据库操作。
TransactionTemplate是Spring提供的一种编程式事务管理方式,它允许我们定义一个事务的边界,并在其中执行数据库操作,然后明确地提交或回滚该事务。
Post AI
博客文章AI生成器
50
查看详情
修改后的测试代码示例:
首先,确保你的测试类中注入了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);
}
} 4. 关键点与注意事项
- 事务隔离级别: 这个问题与数据库的事务隔离级别也有关。在大多数情况下,默认的隔离级别(如READ_COMMITTED)意味着一个事务只能看到其他事务已提交的更改。
- 测试数据管理: 使用TransactionTemplate提交更改后,这些更改会持久化到数据库。在测试结束后,你需要确保这些测试数据被清理,以避免影响其他测试。可以在@AfterEach方法中使用TransactionTemplate来删除测试中创建或修改的数据。
- 何时使用@Transactional: 对于那些不涉及mockMvc或不需要在测试中间提交数据的简单数据库操作测试,@Transactional仍然是方便且推荐的。它提供了自动回滚的便利性。
- 理解Spring Test的事务行为: Spring测试框架默认会在测试方法结束后回滚由@Transactional管理的事务。当我们需要在测试中间强制提交事务时,就必须放弃这种默认行为,转而使用编程式事务管理。
- saveAndFlush()与事务: saveAndFlush()会强制将当前持久化上下文中的更改同步到数据库,但这些更改仍属于当前事务,对其他事务的可见性取决于事务的提交和隔离级别。
在Spring Boot集成测试中,当@Transactional注解与mockMvc结合使用时,可能会遇到事务隔离导致的数据可见性问题。mockMvc请求通常会在一个独立于主测试方法的事务中执行,因此无法看到主事务中尚未提交的数据库更改。通过移除测试方法上的@Transactional注解,并使用TransactionTemplate来显式地管理和提交测试前置的数据库操作,可以确保在mockMvc请求发起时,数据库状态已经更新并对所有新事务可见,从而解决数据不一致的问题,使测试行为符合预期。理解事务边界和隔离级别对于编写健壮的集成测试至关重要。
以上就是Spring Boot集成测试中事务隔离与mockMvc的交互问题及解决方案的详细内容,更多请关注知识资源分享宝库其它相关文章!
相关标签: session ai springboot 为什么 red spring spring boot hibernate 构造函数 Session 线程 数据库 http 大家都在看: Java实现分布式Session共享的多种方案详细对比 Java实现分布式Session的Token方案 Java中Cookie和Session的区别 对比两种会话管理机制的特点 JWT能否实现动态权限变更?与Session机制有何区别? 基于Session的用户登录:服务器端如何真正验证用户身份?






发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。