11.1 RS TCK:测试环境搭建

搭建 Reactive Streams TCK 1.0.4 测试环境:确认它依赖 TestNG 7.3.0、存在独立的 reactive-streams-tck-flow 构件与 FlowPublisherVerification 类,给出普通 Publisher 与 java.util.concurrent.Flow 两套最小依赖和验证基类的真实抽象方法。实测 TestEnvironment 默认超时为 100ms(可用 DEFAULT_TIMEOUT_MILLIS 覆盖),并用一次真实运行说明 38 个用例中 29 通过、9 跳过的构成。

为什么需要 TCK

Reactive Streams 只定义了四个接口和 43 条规则,接口写对不代表行为对。一个 Publisher 完全可以让 onNext 跑在 onSubscribe 之前,可以在 cancel 之后继续发射,也可以把 request(0) 当成无操作——这些都编译通过,但在真实的下游实现里会出错。

RS TCK(Technology Compatibility Kit)把这些规则变成了可执行的断言。判断一个实现是否「合规」,跑 TCK 比读代码可靠。

两个 artifact,不是两个传说

Maven Central 上有两个相关构件,作用不同:

构件用途关键类
reactive-streams-tck验证 org.reactivestreams 类型PublisherVerification、SubscriberWhiteboxVerification、SubscriberBlackboxVerification、IdentityProcessorVerification
reactive-streams-tck-flow验证 java.util.concurrent.Flow 类型FlowPublisherVerification、FlowSubscriberWhiteboxVerification、FlowSubscriberBlackboxVerification、IdentityFlowProcessorVerification

reactive-streams-tck-flow 是真实存在的 1.0.4 构件,不是虚构的。它依赖 reactive-streams-tck,后者再把 TestNG 7.3.0 传递进来(compile scope)。如果你只需要验证自定义的 org.reactivestreams.Publisher,引 reactive-streams-tck 就够了;如果用 JDK 自带的 Flow 接口,再加 reactive-streams-tck-flow。

两个 jar 里都有 org.reactivestreams.tck.flow.support 包,但内容不同:主 TCK 里是规则接口与辅助类,tck-flow 里是与 Flow 类型对接的适配器。

最小依赖

普通 org.reactivestreams 实现:

<dependencies>
    <dependency>
        <groupId>org.reactivestreams</groupId>
        <artifactId>reactive-streams</artifactId>
        <version>1.0.4</version>
    </dependency>

    <dependency>
        <groupId>org.reactivestreams</groupId>
        <artifactId>reactive-streams-tck</artifactId>
        <version>1.0.4</version>
        <scope>test</scope>
    </dependency>
</dependencies>

java.util.concurrent.Flow 实现:

<dependency>
    <groupId>org.reactivestreams</groupId>
    <artifactId>reactive-streams-tck-flow</artifactId>
    <version>1.0.4</version>
    <scope>test</scope>
</dependency>

注意版本:reactive-streams 与 reactive-streams-tck 都用 1.0.4。TCK 的断言会随补丁版本调整,混用版本会制造不必要的变量。

四类验证基类

基类验证对象必须实现的方法
PublisherVerification<T>PublishercreatePublisher(long)、createFailedPublisher()
SubscriberWhiteboxVerification<T>Subscriber(带 probe)createSubscriber(WhiteboxSubscriberProbe<T>)、createElement(int);publisherExecutorService() 有默认实现
SubscriberBlackboxVerification<T>Subscriber(不带 probe)createSubscriber()、createElement(int);publisherExecutorService() 有默认实现
IdentityProcessorVerification<T>ProcessorcreateIdentityProcessor(int)、createFailedPublisher()、createElement(int)、publisherExecutorService()

IdentityProcessorVerification 同时实现 Publisher 与 Subscriber 两侧的规则接口,所以它的测试方法数是四类里最多的。

reactive-streams-tck-flow 里的类把这些抽象方法换成 Flow 版本,例如 FlowPublisherVerification 要求实现 createFlowPublisher(long) 和 createFailedFlowPublisher()。

PublisherVerification 骨架

package com.plume.eventd.core;

import org.reactivestreams.Publisher;
import org.reactivestreams.tck.PublisherVerification;
import org.reactivestreams.tck.TestEnvironment;
import reactor.core.publisher.Flux;

public class RangePublisherTckTest extends PublisherVerification<Integer> {

    public RangePublisherTckTest() {
        super(new TestEnvironment());
    }

    @Override
    public Publisher<Integer> createPublisher(long elements) {
        // 必须恰好发射 elements 个元素
        return Flux.range(0, (int) elements);
    }

    @Override
    public Publisher<String> createFailedPublisher() {
        // 返回 null:依赖失败 Publisher 的可选用例会被跳过
        return null;
    }
}

两个方法的语义:

  • createPublisher(long elements) 返回的 Publisher 必须恰好发射 elements 个元素。TCK 会用 1、3 和更大的数量分别验证;多发一个或少发一个都会在 required_createPublisher1MustProduceAStreamOfExactly1Element 或 required_createPublisher3MustProduceAStreamOfExactly3Elements 里失败。
  • createFailedPublisher() 返回 null 不是失败,而是跳过。TCK 会输出 Skipping because no error state Publisher provided...。如果实现有失败路径,应返回一个「订阅后先 onSubscribe 再 onError」的实例,让 §1.4 与 §1.9 的一部分规则得到真实验证。

TestEnvironment 的默认超时是 100ms

这点和很多旧文章写的 5000ms 不同。new TestEnvironment() 的默认超时来自环境变量 DEFAULT_TIMEOUT_MILLIS,未设置时是 100:

TestEnvironment env = new TestEnvironment();
System.out.println(env.defaultTimeoutMillis());   // 100

在慢速 CI 上,100ms 容易把「实现正确但调度慢」误判成超时。可以显式调大:

super(new TestEnvironment(10_000));   // 默认超时、无信号超时、轮询间隔都用 10s

单参构造器把三个值都设成同一个数;也有 (long, long)、(long, long, long) 重载分别控制默认超时、无信号超时和轮询间隔。

运行与真实计数

用 TestNG 直接运行:

java -cp "testng.jar:jcommander.jar:reactive-streams-tck-1.0.4.jar:\
reactive-streams-1.0.4.jar:reactor-core.jar:out" \
  org.testng.TestNG -testclass RangePublisherTckTest

实测一个合规的 Flux.range 包装、createFailedPublisher() 返回 null:

===============================================
Command line suite
Total tests run: 38, Passes: 29, Failures: 0, Skips: 9
===============================================

38 个用例的构成是:

类别数量说明
实际执行并通过29必测项 + 可选的空流/多订阅测试
因无失败 Publisher 跳过2optional_spec104_*、required_spec109_mayRejectCalls...
标记为 untested_ 而跳过7TCK 未实现或无法自动验证的条目

把 createFailedPublisher() 换成 Flux.error(new RuntimeException("boom")) 后,实测:

Total tests run: 38, Passes: 31, Failures: 0, Skips: 7

多出的 2 个通过正是之前跳过的失败路径用例。所以「29 通过」不等于「29 条规则全部通过」,它只是当前配置下实际执行的用例数;写验收报告时要把跳过原因一起写清楚。

如果你用 Maven Surefire 运行,输出会是 Surefire 自己的格式:

Tests run: 38, Failures: 0, Errors: 0, Skipped: 9

这是正常的。Surefire 对 TestNG 也会打印 Tests run: 汇总,不能据此判断「跑的不是 TCK」。

常见环境错误

找不到 FlowPublisherVerification

java.lang.ClassNotFoundException: org.reactivestreams.tck.flow.FlowPublisherVerification

这个类真实存在,但不在 reactive-streams-tck 里。加上 reactive-streams-tck-flow 1.0.4 依赖即可,不需要自己写适配器。

测试超时

org.testng.internal.thread.ThreadTimeoutException

先确认 Publisher 真的会在超时内终止,以及 request(n) 后确实发射了 n 个元素。常见原因是 createPublisher(n) 发完 n 个元素后没有 onComplete,或者只在第一次 request 时发射、后续请求被忽略。不要第一反应就调大超时。

createFailedPublisher 返回 null

这不是错误,是显式跳过。报告里会写明跳过原因;如果验收要求覆盖 §1.4,就必须返回一个真实的失败 Publisher。

小结

事项结论
测试框架TestNG 7.3.0,由 TCK 传递引入
artifactreactive-streams-tck;Flow 类型另加 reactive-streams-tck-flow
默认超时100ms,环境变量 DEFAULT_TIMEOUT_MILLIS 可覆盖
测试计数默认配置下 38 个用例,29 通过、2 跳过、7 个 untested_
失败 Publisher返回 null 会跳过 2 个用例,不是失败

下一节逐个看 PublisherVerification 实际跑了哪些规则,以及失败时如何定位。

阅读导航

上一篇:10.3 RxJava 3 与 Reactor 的迁移对照

下一篇:11.2 Publisher 合规测试

继续阅读

探索更多技术文章

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

全部文章 返回首页

「java」更多文章

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