用例从哪里来
PublisherVerification 的测试方法名直接编码了规范条款:spec101 对应 §1.1,spec302 对应 §3.2,spec317 对应 §3.17。看失败方法名就能定位到规则,不需要猜。
方法名前缀有四种含义:
| 前缀 | 含义 |
|---|---|
required_ | 必须通过;失败即不合规 |
optional_ | 可选能力;实现不具备时可以失败或跳过 |
stochastic_ | 随机化测试;可用 skipStochasticTests() 关闭 |
untested_ | TCK 暂未实现断言;运行时会跳过 |
untested_ 不是「通过」,也不是「你的实现有问题」,它只是 TCK 没有写对应验证。统计通过率时不能把它算进去。
必测项与对应规则
下面这些都是 required_,方法名来自 1.0.4 的实际字节码,不是转述:
| 测试方法 | 规则 | 验证内容 |
|---|---|---|
required_createPublisher1MustProduceAStreamOfExactly1Element | 配置 | createPublisher(1) 恰好 1 个元素 |
required_createPublisher3MustProduceAStreamOfExactly3Elements | 配置 | createPublisher(3) 恰好 3 个元素 |
required_validate_maxElementsFromPublisher | 配置 | maxElementsFromPublisher() 返回值合法 |
required_validate_boundedDepthOfOnNextAndRequestRecursion | §3.3 | 递归深度配置合法 |
required_spec101_subscriptionRequestMustResultInTheCorrectNumberOfProducedElements | §1.1 | 发射量不超过请求量 |
required_spec102_maySignalLessThanRequestedAndTerminateSubscription | §1.2 | 可以少发并终止 |
required_spec105_mustSignalOnCompleteWhenFiniteStreamTerminates | §1.5 | 有限流以 onComplete 结束 |
required_spec107_mustNotEmitFurtherSignalsOnceOnCompleteHasBeenSignalled | §1.7 | 完成后不再发射 |
required_spec109_subscribeThrowNPEOnNullSubscriber | §1.9 | subscribe(null) 抛 NPE |
required_spec109_mustIssueOnSubscribeForNonNullSubscriber | §1.9 | 先发 onSubscribe |
required_spec109_mayRejectCallsToSubscribeIfPublisherIsUnableOrUnwillingToServeThemRejectionMustTriggerOnErrorAfterOnSubscribe | §1.9 | 拒绝订阅时先 onSubscribe 再 onError |
required_spec302_mustAllowSynchronousRequestCallsFromOnNextAndOnSubscribe | §3.2 | 允许在回调里同步 request |
required_spec303_mustNotAllowUnboundedRecursion | §3.3 | 递归深度有界 |
required_spec306_afterSubscriptionIsCancelledRequestMustBeNops | §3.6 | 取消后 request 是 no-op |
required_spec307_afterSubscriptionIsCancelledAdditionalCancelationsMustBeNops | §3.7 | 重复 cancel 是 no-op |
required_spec309_requestZeroMustSignalIllegalArgumentException | §3.9 | request(0) 发 IllegalArgumentException |
required_spec309_requestNegativeNumberMustSignalIllegalArgumentException | §3.9 | 负数请求发 IllegalArgumentException |
required_spec312_cancelMustMakeThePublisherToEventuallyStopSignaling | §3.12 | 取消后最终停止发射 |
required_spec313_cancelMustMakeThePublisherEventuallyDropAllReferencesToTheSubscriber | §3.13 | 取消后释放订阅者引用 |
required_spec317_mustSupportAPendingElementCountUpToLongMaxValue | §3.17 | 需求可累积到 Long.MAX_VALUE |
required_spec317_mustSupportACumulativePendingElementCountUpToLongMaxValue | §3.17 | 累计需求不溢出 |
required_spec317_mustNotSignalOnErrorWhenPendingAboveLongMaxValue | §3.17 | 超过上限不报错 |
可选项主要是空流完成(optional_spec105_emptyStreamMustTerminateBySignallingOnComplete)、失败路径(optional_spec104_mustSignalOnErrorWhenFails)和多订阅者一致性(optional_spec111_*)。
一次真实运行
用一个合规的 Flux.range 包装做基线,createFailedPublisher() 返回 null:
===============================================
Command line suite
Total tests run: 38, Passes: 29, Failures: 0, Skips: 9
===============================================
9 个跳过里:
- 7 个是
untested_*:spec106、spec107(错误分支)、spec108、spec109(subscribe 抛非致命异常)、spec110、spec304、spec305; - 2 个因为
createFailedPublisher()返回null:optional_spec104_mustSignalOnErrorWhenFails和required_spec109_mayRejectCallsToSubscribe...。
把 createFailedPublisher() 改成 Flux.error(new RuntimeException("boom")) 后:
Total tests run: 38, Passes: 31, Failures: 0, Skips: 7
多出的 2 个通过就是失败路径用例。这个对照说明:跳过数不是固定的,它取决于你提供了哪些可选能力。
还有一个影响用例行为的配置是 maxElementsFromPublisher()。它的默认值是 Long.MAX_VALUE - 1,TCK 会据此决定部分测试的元素规模。实现的缓冲或计数器承受不了超大值时,应显式覆盖成一个现实上界:
@Override
public long maxElementsFromPublisher() {
return 10_000;
}
失败时怎么读信息
TCK 的断言信息通常直接说明现象。下面是两条典型输出(其中的超时数字随 TestEnvironment 配置变化)。
多发一个元素(createPublisher(n) 返回了 n+1 个):
java.lang.AssertionError: Expected end-of-stream but got element [2]
失败方法是 required_createPublisher1MustProduceAStreamOfExactly1Element。修复不是改测试,而是让 createPublisher(1) 只发 1 个并在之后 onComplete。
忽略 request(0)(没有发 IllegalArgumentException):
java.lang.AssertionError: Expected onError(java.lang.IllegalArgumentException) within 100 ms
失败方法是 required_spec309_requestZeroMustSignalIllegalArgumentException。修复是在 request 入口检查 n <= 0,并调用 onError(new IllegalArgumentException(...)),而不是静默返回或抛异常。
再看两个容易被误判的失败:
| 失败方法 | 常见原因 | 修复方向 |
|---|---|---|
required_spec107_mustNotEmitFurtherSignalsOnceOnCompleteHasBeenSignalled | 完成标志未检查,异步线程继续发 | 在每次发射前检查 terminated 标志 |
required_spec312_cancelMustMakeThePublisherToEventuallyStopSignaling | cancel() 只置标志,但已排队的任务仍继续 onNext | 在 drain 循环里检查取消,并停止补充任务 |
required_spec313_cancelMustMakeThePublisherEventuallyDropAllReferencesToTheSubscriber | Subscription 持有 subscriber 的强引用 | 取消时把引用置空 |
required_spec313 的失败通常不是逻辑错,而是内存泄漏:Publisher 在取消后仍持有 Subscriber,导致下游对象无法回收。这类问题在生产里比测试失败更隐蔽。
异步 Publisher 的额外注意
同步 Publisher 的测试基本是确定性的;异步 Publisher 会引入两个变量:
- 信号必须最终可见。TCK 在测试线程上轮询
TestEnvironment的探针;异步线程发完onComplete后如果没有正确的内存可见性(例如用普通字段做完成标志),测试线程可能一直等不到终止信号。 - 超时要留足余量。默认 100ms 对线程池调度偏紧,建议在异步场景用
new TestEnvironment(5_000)起步,再根据 CI 稳定性调整,而不是无条件调到几十秒。
PublisherVerification 没有 publisherExecutorService() 钩子——那是 Subscriber 两类基类才有的。异步 Publisher 的线程池由测试类自己创建,也由测试类在 @AfterClass 里关闭,否则 Surefire 可能挂住。
public class AsyncRangePublisherTckTest extends PublisherVerification<Integer> {
private ExecutorService executor;
public AsyncRangePublisherTckTest() {
super(new TestEnvironment(5_000));
}
@Override
public long maxElementsFromPublisher() {
// TCK 的 spec317 用例会按 Long.MAX_VALUE 附近的规模调用 createPublisher;
// AsyncRangePublisher 用 int 计数,必须先声明现实上界,否则 (int) 溢出
return 10_000;
}
@Override
public Publisher<Integer> createPublisher(long elements) {
int count = Math.toIntExact(elements);
return new AsyncRangePublisher(1, count + 1, executor());
}
@Override
public Publisher<Integer> createFailedPublisher() {
return null;
}
@AfterClass
public void tearDown() {
if (executor != null) {
executor.shutdownNow();
}
}
private ExecutorService executor() {
if (executor == null) {
executor = Executors.newSingleThreadExecutor();
}
return executor;
}
}
上面的 executor() 是示例自己的资源管理,@AfterClass 的 tearDown() 负责关闭,避免 Surefire 进程挂住。shutdownNow() 会中断仍在运行的任务;如果测试需要等任务结束,改用 shutdown() 加 awaitTermination()。
小结
| 关注点 | 结论 |
|---|---|
| 方法名 | specNNN 直接对应规范条款,失败时先看编号 |
| 必测项 | 22 个 required_;默认配置下 21 个通过,1 个因未提供失败 Publisher 跳过 |
| 默认运行 | 38 个用例,29 通过、7 个 untested_、2 个因无失败 Publisher 跳过 |
| 失败 Publisher | 提供后多 2 个用例通过 |
| 元素数量 | createPublisher(n) 必须恰好 n 个 |
| 非正请求 | request(n<=0) 必须 onError(IllegalArgumentException) |
| 取消 | 停止发射并释放订阅者引用 |
阅读导航
下一篇:11.3 Subscriber 与 Processor 合规测试
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。