
事务隔离与集成测试中的数据可见性
在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允许我们在代码块内执行事务性操作,并可以明确地控制事务的提交或回滚。
步骤:
- 从测试方法上移除@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
@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);
});
}
}代码解释:
- @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模拟的请求能够正确地与数据库的最新状态进行交互,从而编写出更准确、更可靠的集成测试。










