为什么需要 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> | Publisher | createPublisher(long)、createFailedPublisher() |
SubscriberWhiteboxVerification<T> | Subscriber(带 probe) | createSubscriber(WhiteboxSubscriberProbe<T>)、createElement(int);publisherExecutorService() 有默认实现 |
SubscriberBlackboxVerification<T> | Subscriber(不带 probe) | createSubscriber()、createElement(int);publisherExecutorService() 有默认实现 |
IdentityProcessorVerification<T> | Processor | createIdentityProcessor(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 跳过 | 2 | optional_spec104_*、required_spec109_mayRejectCalls... |
标记为 untested_ 而跳过 | 7 | TCK 未实现或无法自动验证的条目 |
把 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 传递引入 |
| artifact | reactive-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 的迁移对照
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。