如何正确编写带事务支持的Quartz调度器集成测试
问题根因分析
- 事务隔离级别导致的可见性问题:你配置的
REPEATABLE_READ隔离级别下,Quartz任务线程的事务如果在测试代码保存Event的事务提交前就启动,会基于事务启动时的快照读数据,看不到后续提交的Event记录,因此只会生成1条甚至0条Task。 - 固定等待逻辑不可靠:
Thread.sleep只能固定等待时长,无法保证调度任务刚好在等待周期内完成全量执行,也无法保证Event的事务提交对任务线程可见。 enable_lazy_load_no_trans属于高危临时配置:该配置会为每个懒加载操作创建临时会话,本身就极易引发数据不一致、会话泄漏等隐性问题,不适合在测试或生产环境使用。
修复方案
1. 替换不可靠的固定等待逻辑
使用条件等待工具Awaitility替代Thread.sleep,只有当符合预期的前置条件满足后才执行断言,避免提前读取未生成完成的Task数据:
<!-- 引入Awaitility依赖,版本可自行适配SpringBoot版本 --> <dependency> <groupId>org.awaitility</groupId> <artifactId>awaitility</artifactId> <scope>test</scope> </dependency>
修改测试代码中的等待逻辑:
import static org.awaitility.Awaitility.await; import java.time.Duration; @SneakyThrows @Test public void taskGenerationBeanTest() { RequestData request = requestBuilder(str(), str(), str()).build(); String orderId = String.valueOf(UUID.randomUUID()); Event event1 = getEvent("type1", request, orderId); Event event2 = getEvent("type2", request, orderId); eventRepository.save(event1); eventRepository.save(event2); // 等待最多30s,每500ms轮询一次,直到Task数量符合预期 await().atMost(Duration.ofSeconds(30)) .pollInterval(Duration.ofMillis(500)) .until(() -> taskRepository.count() >= 2); List<Task> actual = taskRepository.findAll(); assertAll( () -> assertEquals(2, actual.size()), () -> assertTrue(actual.stream().anyMatch(t -> "type1".equals(t.getEvent().getEventType()))), () -> assertEquals(event1.getOrderId(), actual.get(0).getEvent().getOrderId()) ); }
2. 解决事务可见性问题
如果业务没有强制要求REPEATABLE_READ隔离级别,可将任务方法的事务隔离级别调整为READ_COMMITTED,保证每次都能读取到已提交的最新数据:
@Override @Transactional(isolation = Isolation.READ_COMMITTED) public int createAndSaveTasks(EventType eventType) { // 原有逻辑保持不变 List<Task> tasks = eventService.getEvent(eventType) .stream() .map(Task::new) .collect(Collectors.toList()); taskRepository.saveAll(tasks); return tasks.size(); }
如果必须保留REPEATABLE_READ隔离级别,可在读取Event前手动刷新、清空持久化上下文,强制读取最新数据:
@PersistenceContext private EntityManager entityManager; @Override @Transactional(isolation = Isolation.REPEATABLE_READ) public int createAndSaveTasks(EventType eventType) { entityManager.flush(); entityManager.clear(); List<Task> tasks = eventService.getEvent(eventType) .stream() .map(Task::new) .collect(Collectors.toList()); taskRepository.saveAll(tasks); return tasks.size(); }
3. 正确解决懒加载异常
移除spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true配置,通过关联查询一次性加载关联属性,避免无会话场景下的懒加载问题:
// 在TaskRepository中重写findAll方法,添加EntityGraph注解关联加载Event @EntityGraph(attributePaths = "event") List<Task> findAll();
4. 可选优化:提高测试确定性
如果不需要验证Quartz cron触发器本身的调度逻辑,可直接在测试中手动调用createAndSaveTasks方法两次(对应type1、type2两种事件类型),完全避免调度延迟、事务提交时序带来的不稳定问题。
内容的提问来源于stack exchange,提问作者petrov.aleksandr
相关产品推荐
相关产品推荐

