11.2 Publisher 合规测试

按真实方法名梳理 PublisherVerification 的必测项:createPublisher 精确元素数、spec101 请求与发射数量、spec102 可少发、spec103 串行信号、spec105 完成信号、spec107 终止后静默、spec109 订阅规则、spec302 同步 request、spec303 递归深度、spec306/307 取消后 no-op、spec309 非正请求、spec312/313 取消语义、spec317 长整型需求。实测 38 个用例中 29 通过、7 个 untested、2 个因无失败 Publisher 跳过,并给出两条真实失败信息与定位方法。

用例从哪里来

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.9subscribe(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.9request(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_cancelMustMakeThePublisherToEventuallyStopSignalingcancel() 只置标志,但已排队的任务仍继续 onNext在 drain 循环里检查取消,并停止补充任务
required_spec313_cancelMustMakeThePublisherEventuallyDropAllReferencesToTheSubscriberSubscription 持有 subscriber 的强引用取消时把引用置空

required_spec313 的失败通常不是逻辑错,而是内存泄漏:Publisher 在取消后仍持有 Subscriber,导致下游对象无法回收。这类问题在生产里比测试失败更隐蔽。

异步 Publisher 的额外注意

同步 Publisher 的测试基本是确定性的;异步 Publisher 会引入两个变量:

  1. 信号必须最终可见。TCK 在测试线程上轮询 TestEnvironment 的探针;异步线程发完 onComplete 后如果没有正确的内存可见性(例如用普通字段做完成标志),测试线程可能一直等不到终止信号。
  2. 超时要留足余量。默认 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.1 RS TCK:测试环境搭建

下一篇:11.3 Subscriber 与 Processor 合规测试

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. 15.3 全书总结与学习路径回顾
  2. 15.2 响应式流处理的演进趋势
  3. 15.1 引擎选型指南