6.1 测试策略


6.1 测试策略

本节摘要:同步测试断言最终值不够;响应式要断言信号顺序、时间与错误。Reactor 的 StepVerifier + VirtualTimeScheduler 是标准工具,可把 delayHours 压到毫秒级。

本节导读

  1. 用 StepVerifier 验证 onNext → onComplete
  2. 用 VirtualTime 测试 timeoutinterval
  3. 说明 StepVerifierverifyCompleteverifyError 的差异

一、StepVerifier 基础

StepVerifier.create(Flux.just(1, 2, 3)) .expectNext(1, 2, 3) .verifyComplete(); StepVerifier.create(Mono.error(new IllegalStateException("x"))) .expectError(IllegalStateException.class) .verify();

验证的是 Publisher 契约:顺序、完成、错误类型。

传统单元测试断言「返回值等于什么」,响应式测试断言的是**「信号以什么顺序、什么时间、什么终止方式流动」**。StepVerifier 把订阅过程重放为一步步断言:expectNext 检查下一个元素,verifyComplete 检查流以完成信号结束,expectError 检查流以特定错误结束。这套模型的关键是:你没有真正「跑」流,而是让 StepVerifier 订阅它并逐步验证每一个信号——因此即使流里有异步调度,测试也能按声明的步骤确定性地推进。

// 验证序列 + 数量 StepVerifier.create(Flux.range(1, 10)) .expectNextCount(10) // 断言个数 .verifyComplete(); // 验证元素匹配谓词 StepVerifier.create(Flux.just("a", "ab", "abc")) .expectNextMatches(s -> s.length() > 0) .expectNextCount(2) .verifyComplete();

二、虚拟时间

StepVerifier.withVirtualTime(() -> Flux.interval(Duration.ofHours(1)).take(2)) .expectSubscription() .thenAwait(Duration.ofHours(2)) .expectNext(0L, 1L) .verifyComplete();

thenAwait 推进虚拟时钟,无需真等 2 小时——SOURCE 2.2 称时间为可编程资源

虚拟时间能工作的前提:被测流的时间操作(intervaldelaytimeout)都经过 Scheduler 抽象。StepVerifier.withVirtualTime 会替换掉全局的默认 Scheduler,测试里的「2 小时」只是虚拟时钟上的刻度,实际执行是瞬间的。这让时间敏感的行为(超时、节流、定时器)第一次可以被快速、确定地测试。

测试时间相关行为

被测行为 断言方式
timeout 触发 thenAwait(超时时间) + expectError(TimeoutException)
interval 周期 thenAwait(N * 周期) + expectNextCount(N)
delay 延迟 thenAwait(延迟)expectNext
debounce 防抖 thenAwait(间隔) 验证合并

三、测什么

场景 断言
重试 expectNext + 错误后再次 next
背压 thenRequest(n)
取消 thenCancel() + verify

RxJS 对照:TestScheduler + marble diagrams 承担类似角色。

// 测试背压:验证 request 后的信号节奏 StepVerifier.create(Flux.range(1, 5), 0) // 初始 0 个请求 .expectSubscription() .thenRequest(2) .expectNext(1, 2) // 请求 2 个来 2 个 .thenRequest(3) .expectNext(3, 4, 5) .verifyComplete(); // 测试取消:验证 cancel 后不再有信号 StepVerifier.create(Flux.interval(Duration.ofMillis(100))) .thenAwait(Duration.ofMillis(350)) .thenCancel() // 取消订阅 .verify(); // 验证终止

背压与取消是响应式独有的测试维度:thenRequest(n) 逐批验证「请求多少、收到多少」,确保流遵守信用契约;thenCancel() 验证取消后流立即停止并释放资源——这两类测试在命令式代码里根本没有对应物,却是响应式系统资源安全的最后防线。

复杂 retryWhen 的测试

StepVerifier.withVirtualTime(() -> flakyRpc() .retryWhen(Retry.backoff(3, Duration.ofMillis(100)))) .expectSubscription() .thenAwait(Duration.ofMillis(100)) // 第一次退避 .thenAwait(Duration.ofMillis(200)) // 第二次退避 .thenAwait(Duration.ofMillis(400)) // 第三次退避 .expectNext("result") .verifyComplete();

retryWhen 的退避节奏拆成 thenAwait 逐步推进,是响应式测试中最能体现「虚拟时间价值」的场景——真实环境等 100+200+400ms 无妨,但配合验证退避次数与最终成功,虚拟时间让「重试策略是否正确」变成一分钟能跑一百次的确定性测试。

⚠️ 常见坑:生产链带 publishOn 但未在测试中 await,偶发 flaky。

Marble 图与 RxJS TestScheduler

RxJS 生态用 marble 图(弹珠图)描述流的行为,配合 TestScheduler 在测试中推进虚拟时间:

// RxJS 测试:marble 语法 + TestScheduler import { TestScheduler } from 'rxjs/testing'; const scheduler = new TestScheduler((actual, expected) => { expect(actual).toEqual(expected); // 深度比较 marble 序列 }); it('debounceTime 应该合并 300ms 内的输入', () => { scheduler.run(({ cold, expectObservable, time }) => { const input = '---a---b---|'; // 输入流 const expected = '300ms ---a---b---|'; // 期望输出 const source = cold(input).pipe(debounceTime(300)); expectObservable(source).toBe(expected); }); });

Marble 图用字符直接表达时间轴:- 表示 1 帧(测试里的时间单位),字母表示元素,| 表示完成,# 表示错误。这比手写一串 thenAwait 更紧凑直观,尤其适合断言时间相关的操作符组合。Reactor 没有内置 marble 语法,但 StepVerifier.withVirtualTime 提供了同等表达力——两者理念完全一致:用虚拟时间把「时钟」变成可断言的输入。

测试金字塔在响应式中的形态

层级 覆盖内容 工具 占比
算子单测 单一算子语义 StepVerifier 最多
组合测试 多算子/业务链 StepVerifier + VirtualTime 较多
集成测试 真实 Scheduler/网络 真实运行时 + timeout 少量
契约测试 背压/取消跨组件 TCK + 集成 最少

原则与命令式相同:越快越多的测试越底层。 响应式特有的补充是——大部分算子组合都可以在虚拟时间下秒级验证,所以「组合测试」应当占很大比重,而不是等到集成环境才暴露时间问题。

重点提炼

  • 测信号序列,不只测返回值
  • VirtualTime 是响应式 TDD 基石
  • 复杂 retryWhen 应拆成 marble 用例

测试策略总表

测试类型 工具 覆盖
信号序列 StepVerifier 顺序、数量、终止
时间行为 withVirtualTime timeout/delay/interval
背压 thenRequest 信用契约
取消 thenCancel 资源释放
组合并发 StepVerifier + flatMap 并发度、顺序

一句话总结:响应式测试把「异步与时间」变成可断言的契约——StepVerifier 断言信号序列,虚拟时间断言时间行为,thenRequest/thenCancel 断言背压与取消,四者合起来才是完整的响应式质量保障。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U