Spring Reactive WebClient重试逻辑用StepVerifier虚拟时间测试卡住问题
问题根因
Retry.fixedDelay默认使用Reactor内置的Schedulers.parallel()调度器执行延迟逻辑,而StepVerifier.withVirtualTime只能接管虚拟时间调度器的时序,默认调度器的延迟不受虚拟时间控制,会按照真实系统时间等待2秒/次,导致测试看起来卡在重试环节。- 如果你使用OkHttp的MockWebServer做接口模拟,会产生真实的TCP网络IO请求,这部分逻辑运行在独立IO线程,不受Reactor虚拟时间管控,也会引发时序错乱导致测试卡住。
解决方案
方案1:修改重试逻辑支持调度器注入(推荐,兼顾逻辑正确性和测试能力)
业务代码中给Retry配置显式指定可替换的调度器,测试时替换为虚拟时间调度器即可:
// 业务代码新增可配置的调度器 private Scheduler retryScheduler = Schedulers.parallel(); // 提供setter方法供测试注入自定义调度器 public void setRetryScheduler(Scheduler retryScheduler) { this.retryScheduler = retryScheduler; } public Mono<Todo> getTodoWithSimpleRetry(String id) { RetryBackoffSpec retrySpec = Retry.fixedDelay(3, Duration.ofSeconds(2)) .scheduler(retryScheduler) // 新增这行指定调度器 .doBeforeRetry(beforeRetry -> { log.info("Attempt to retry: " + (beforeRetry.totalRetries() + 1)); }).doAfterRetry(afterRetry -> { log.error("After retry {}", afterRetry.failure().getMessage()); }) .onRetryExhaustedThrow(((retrySpec1, retrySignal) -> { throw new TodoServiceException("Max retry exceeded. Todo Service is not available", HttpStatus.SERVICE_UNAVAILABLE.value()); })); return webClient.get() .uri("/todo/{id}", id) .retrieve() .bodyToMono(Todo.class) .retryWhen(retrySpec); }
测试代码中注入虚拟时间调度器:
private VirtualTimeScheduler virtualTimeScheduler; @BeforeEach void setUp() { virtualTimeScheduler = VirtualTimeScheduler.create(); todoWebClient.setRetryScheduler(virtualTimeScheduler); } @Test void testRetry() { mockBackEnd.enqueue(new MockResponse().setResponseCode(500)); mockBackEnd.enqueue(new MockResponse().setResponseCode(500)); mockBackEnd.enqueue(new MockResponse().setResponseCode(500)); mockBackEnd.enqueue(new MockResponse().setResponseCode(500)); StepVerifier.withVirtualTime(() -> todoWebClient.getTodoWithSimpleRetry("1")) .expectSubscription() .thenAwait(Duration.ofSeconds(6)) // 直接快进总等待时间即可 .expectError(TodoServiceException.class) .verify(); }
方案2:替换MockWebServer为Spring MockWebClient
使用Spring自带的MockWebClient做接口模拟,不需要真实网络IO,完全适配Reactor的调度器体系,避免外部线程干扰虚拟时间控制:
@Test void testRetry() { MockWebClient mockWebClient = MockWebClient.create(); TodoWebClient todoWebClient = new TodoWebClient(mockWebClient); // 配置4次500响应,对应1次初始请求+3次重试 mockWebClient.get().uri("/todo/1").times(4) .respond(response -> response.statusCode(500)); StepVerifier.withVirtualTime(() -> todoWebClient.getTodoWithSimpleRetry("1")) .expectSubscription() .thenAwait(Duration.ofSeconds(6)) .expectError(TodoServiceException.class) .verify(); }
方案3:快速验证方案(仅验证异常和重试次数)
如果不需要校验重试间隔的正确性,可以直接将测试环境的重试间隔改为0,跳过等待逻辑:
// 测试时用无延迟的重试规则代替固定延迟重试 RetryBackoffSpec testRetrySpec = Retry.max(3) .doBeforeRetry(beforeRetry -> log.info("Attempt to retry: " + (beforeRetry.totalRetries() + 1))) .onRetryExhaustedThrow(((retrySpec1, retrySignal) -> { throw new TodoServiceException("Max retry exceeded. Todo Service is not available", HttpStatus.SERVICE_UNAVAILABLE.value()); }));
内容的提问来源于stack exchange,提问作者ExcepOra
相关产品推荐
相关产品推荐

