|
|
@@ -0,0 +1,172 @@
|
|
|
+package com.payment.platform;
|
|
|
+
|
|
|
+import com.payment.platform.module.payment.leshouying.scheduler.LsyReconcileScheduler;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.boot.test.context.SpringBootTest;
|
|
|
+import org.springframework.boot.test.context.TestConfiguration;
|
|
|
+import org.springframework.context.ApplicationContext;
|
|
|
+import org.springframework.context.annotation.Bean;
|
|
|
+import org.springframework.scheduling.TaskScheduler;
|
|
|
+import org.springframework.scheduling.Trigger;
|
|
|
+
|
|
|
+import java.time.Clock;
|
|
|
+import java.time.Duration;
|
|
|
+import java.time.Instant;
|
|
|
+import java.util.concurrent.ScheduledFuture;
|
|
|
+import java.util.concurrent.atomic.AtomicInteger;
|
|
|
+
|
|
|
+import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 上下文加载测试 —— 全仓**第一条**碰 Spring 容器的测试(裁定 R-5)。
|
|
|
+ *
|
|
|
+ * 【为什么它存在】台账 §3.3 实测:`src/test` 下 39 个测试文件里,
|
|
|
+ * `@SpringBootTest` / `@WebMvcTest` / `@DataJpaTest` / `@MybatisPlusTest` /
|
|
|
+ * `@ContextConfiguration` / `@ExtendWith(SpringExtension.class)` **六种容器入口全部零命中** ——
|
|
|
+ * 全部单测走 Mockito,**不碰容器**。于是每个新增的 `@Service` / `@RestController` / `@Mapper`
|
|
|
+ * 的装配(缺 bean / bean 二义 / mapper 不在 `@MapperScan` 范围)**只在真正启动那一刻**
|
|
|
+ * 第一次被检查,而测试套件保持全绿。
|
|
|
+ *
|
|
|
+ * 【裁定 R-5 的约束,写在最前面】**不得为了让它绿而 mock 掉容器** ——
|
|
|
+ * 那就把它要防的东西防掉了。
|
|
|
+ *
|
|
|
+ * 【实测(2026-09-22)它需要什么】真连 PostgreSQL。本仓 `src/test/resources/` **不存在**,
|
|
|
+ * 测试直接吃 `src/main/resources/application.yml`(线上那份),其中 Druid starter 在
|
|
|
+ * 建 `dataSource` bean 时就会去拿连接(不是 Hikari 那种懒到首次使用的语义),
|
|
|
+ * 所以**跑本用例必须挂着库隧道**。这不是推测,是探针量出来的:
|
|
|
+ * ① 把 `spring.datasource.druid.url` 指向死端口 ⇒ 上下文加载**失败**,
|
|
|
+ * 根因是 `dataSource` bean 本身(`...DruidDataSourceAutoConfigure`)抛
|
|
|
+ * `PSQLException: Connection refused`;
|
|
|
+ * ② 把 `spring.data.redis.port` 指向死端口 ⇒ 上下文**照常加载成功**(Redis 非装配必需)。
|
|
|
+ *
|
|
|
+ * 【为什么要换掉 `TaskScheduler`】全仓有**两个** `@EnableScheduling`
|
|
|
+ * (`PaymentApplication` 与 `core/scheduler/ScheduledTasks` —— 只摘掉一个调度仍然活着,
|
|
|
+ * 别拿它当「关掉调度」的开关),而 7 个类、7 个 `@Scheduled` 方法
|
|
|
+ * **没有一个设 `initialDelay`** ⇒ 默认 0 ⇒
|
|
|
+ * **上下文一 refresh 就立刻跑一轮**。实测:不换的话,本用例运行期间会真的
|
|
|
+ * 向**上游支付网关**发 HTTP(`/v3/queryorder`、`/v3/qrclose` 各一次,`scheduling-1` 线程上),
|
|
|
+ * 并真的写 Redis。测试套件不该在每次 `mvn test` 时朝支付网关发请求 —— 这是本测试里
|
|
|
+ * 唯一一处「不照裁定字面、按裁定意图落地」的地方,理由与读数见台账 §八 R-5。
|
|
|
+ *
|
|
|
+ * ⚠ 换 `TaskScheduler` **不是**上面禁的「mock 掉容器」:容器照常 refresh,
|
|
|
+ * 每一个业务 bean(含下面注入的那个调度类)都照常创建、照常注入、照常参与装配检查,
|
|
|
+ * 被替掉的只是基础设施里的**调度器**;换句话说,本用例仍然会因装配缺陷而红,
|
|
|
+ * 只是不会因为「跑了一轮定时任务」而向外发请求。
|
|
|
+ */
|
|
|
+@SpringBootTest
|
|
|
+class PaymentApplicationContextTest {
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private ApplicationContext context;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private TaskScheduler scheduler;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private LsyReconcileScheduler reconcileScheduler;
|
|
|
+
|
|
|
+ /** 只登记、不执行。用来在测试上下文里掐掉全部定时任务。 */
|
|
|
+ static class NeverRunsSchedules implements TaskScheduler {
|
|
|
+
|
|
|
+ /** 被登记过的任务数 —— 供「调度确实还在被登记」这条正对照使用。 */
|
|
|
+ private final AtomicInteger registered = new AtomicInteger();
|
|
|
+
|
|
|
+ int registered() {
|
|
|
+ return registered.get();
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Clock getClock() {
|
|
|
+ return Clock.systemDefaultZone();
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public ScheduledFuture<?> schedule(Runnable task, Trigger trigger) {
|
|
|
+ registered.incrementAndGet();
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public ScheduledFuture<?> schedule(Runnable task, Instant startTime) {
|
|
|
+ registered.incrementAndGet();
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public ScheduledFuture<?> scheduleAtFixedRate(Runnable task, Duration period) {
|
|
|
+ registered.incrementAndGet();
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public ScheduledFuture<?> scheduleAtFixedRate(Runnable task, Instant startTime, Duration period) {
|
|
|
+ registered.incrementAndGet();
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public ScheduledFuture<?> scheduleWithFixedDelay(Runnable task, Duration delay) {
|
|
|
+ registered.incrementAndGet();
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public ScheduledFuture<?> scheduleWithFixedDelay(Runnable task, Instant startTime, Duration delay) {
|
|
|
+ registered.incrementAndGet();
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @TestConfiguration
|
|
|
+ static class MuteScheduling {
|
|
|
+
|
|
|
+ /** 名字必须是 `taskScheduler` —— `ScheduledAnnotationBeanPostProcessor` 优先按这个名字找。 */
|
|
|
+ @Bean
|
|
|
+ TaskScheduler taskScheduler() {
|
|
|
+ return new NeverRunsSchedules();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void contextLoads() {
|
|
|
+ // 本用例的断言就是「上下文装配成功」本身:`@SpringBootTest` 在 JUnit 的
|
|
|
+ // `@BeforeEach` 之前就把容器 refresh 完,装配失败会**在进入本方法之前**抛异常。
|
|
|
+ // 所以这里不写断言不是遗漏 —— 写一条假的 assertTrue(true) 反而是噪音。
|
|
|
+ //
|
|
|
+ // ⚠ 但这也意味着:**它的红灯长得和「测试写错了」一样**,都发生在进方法之前。
|
|
|
+ // 判读时看异常类型:`UnsatisfiedDependencyException` / `NoSuchBeanDefinitionException`
|
|
|
+ // / `BeanDefinitionOverrideException` 是装配缺陷(本用例要抓的),
|
|
|
+ // 连不上库是环境问题(本用例的已知依赖,见类注释)。
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 三条正对照,各挡一种「本用例悄悄变成空转」的方式。
|
|
|
+ *
|
|
|
+ * 全是正对照、没有一条负断言是有意的:本类要防的事情(「不装配」/「偷偷跑定时任务」)
|
|
|
+ * 都是**不发生**的事件,而「不发生」是自证的 —— 只有把「该在的还在」钉住,
|
|
|
+ * 「没发生」才不是空话。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void schedulingIsStillWiredButNothingRuns() {
|
|
|
+ // ① 我那个调度器**就是**容器在用的那个 ⇒ 没有任何别的调度器能把任务真的跑起来。
|
|
|
+ // 挡的是「bean 没被采纳、真调度器照跑」——那种情况下本类会向支付网关发请求却仍然绿。
|
|
|
+ assertEquals(NeverRunsSchedules.class, scheduler.getClass(),
|
|
|
+ "容器里的 TaskScheduler 不是本测试替换的那个,定时任务可能真的跑起来了");
|
|
|
+
|
|
|
+ // ② `@Scheduled` 注解**仍然在被发现**(登记动作确实发生过)。
|
|
|
+ // 挡的是「调度整个失效了」——那时本类没向外交发请求是**因为压根没调度**,
|
|
|
+ // 而不是因为被掐住了,绿灯会变成一句空话(例如 `@EnableScheduling` 被摘掉)。
|
|
|
+ assertTrue(((NeverRunsSchedules) scheduler).registered() > 0,
|
|
|
+ "一个定时任务都没被登记 —— 调度很可能已经失效,本用例的『不发外部请求』是空转");
|
|
|
+
|
|
|
+ // ③ 调度类本身**照常装配**(R-5 要抓的那类缺陷仍在本用例的覆盖范围内)。
|
|
|
+ // 挡的是「为了不发请求,干脆把调度 bean 从容器里摘掉」这种走样 ——
|
|
|
+ // 那样一来 R-5 的装配网就漏掉了它们。
|
|
|
+ assertNotNull(reconcileScheduler,
|
|
|
+ "调度类没有被装配 —— 装配检查漏了它们");
|
|
|
+ assertNotNull(context.getBean(LsyReconcileScheduler.class));
|
|
|
+ }
|
|
|
+}
|