两套基类,差别在「谁能控制订阅」
TCK 对 Subscriber 提供两个基类:
| 基类 | 订阅控制 | 适合什么实现 |
|---|---|---|
SubscriberWhiteboxVerification<T> | 测试通过 probe 直接控制 request 和 cancel | 自己能管理订阅状态、想让 TCK 精确驱动信号的 Subscriber |
SubscriberBlackboxVerification<T> | 测试只能发信号,请求依赖 triggerRequest | 不便暴露内部状态,或想模拟真实 Publisher 行为 |
两者验证的规则高度重叠,白盒多出的能力是「精确控制」:TCK 可以只请求 1 个元素,也可以在你以为订阅还活着的时候取消。代价是你要在 Subscriber 里多写几行探测回调。
白盒的真实抽象方法只有两个
不少旧文章会把 publisherExecutorService() 也算成必须实现的方法。1.0.4 的实际签名不是这样:
public abstract Subscriber<T> createSubscriber(WhiteboxSubscriberProbe<T> probe);
public abstract T createElement(int element); // 来自 WithHelperPublisher
// 有默认实现,可以不覆写
public ExecutorService publisherExecutorService();
publisherExecutorService() 的默认实现由 setUp() 创建 4 线程池、tearDown() 关闭。只有需要指定线程模型时才覆写它,而且覆写后要自己负责关闭。
createElement(int) 负责生成第 n 个测试元素。TCK 不关心元素内容,只要求同一个 element 索引每次生成可比较的值。
probe 与 puppet 的职责
WhiteboxSubscriberProbe<T> 有四个注册方法:
| 方法 | 调用时机 |
|---|---|
registerOnSubscribe(SubscriberPuppet) | 收到 onSubscribe 后 |
registerOnNext(T) | 处理完 onNext 后 |
registerOnError(Throwable) | 处理完 onError 后 |
registerOnComplete() | 处理完 onComplete 后 |
只调用自己的 onNext 而不告诉 probe,TCK 会认为信号没有到达,测试最终以超时失败。这是白盒最容易踩的坑:探测回调是契约的一部分,不是可选的日志。
registerOnSubscribe 接收的 SubscriberPuppet 有两个方法:
| 方法 | TCK 的意图 |
|---|---|
triggerRequest(long n) | 要求 Subscriber 向上游请求 n 个 |
signalCancel() | 要求 Subscriber 取消订阅 |
一个能跑通的白盒骨架
下面这段代码基于 Reactor 3.8.8 的 BaseSubscriber,只实现两个抽象方法:
import org.reactivestreams.Subscription;
import org.reactivestreams.tck.SubscriberWhiteboxVerification;
import org.reactivestreams.tck.TestEnvironment;
import org.reactivestreams.tck.SubscriberWhiteboxVerification.SubscriberPuppet;
import org.reactivestreams.tck.SubscriberWhiteboxVerification.WhiteboxSubscriberProbe;
import reactor.core.publisher.BaseSubscriber;
public class EventSubscriberWhiteboxTckTest
extends SubscriberWhiteboxVerification<String> {
public EventSubscriberWhiteboxTckTest() {
super(new TestEnvironment(5_000));
}
@Override
public BaseSubscriber<String> createSubscriber(WhiteboxSubscriberProbe<String> probe) {
return new BaseSubscriber<String>() {
@Override
protected void hookOnSubscribe(Subscription s) {
probe.registerOnSubscribe(new SubscriberPuppet() {
@Override public void triggerRequest(long n) { request(n); }
@Override public void signalCancel() { cancel(); }
});
request(Long.MAX_VALUE);
}
@Override protected void hookOnNext(String value) { probe.registerOnNext(value); }
@Override protected void hookOnError(Throwable error) { probe.registerOnError(error); }
@Override protected void hookOnComplete() { probe.registerOnComplete(); }
};
}
@Override
public String createElement(int element) {
return "elem" + element;
}
}
用 BaseSubscriber 不是为了省代码,而是因为它已经处理了两件容易写错的事:
onSubscribe用Operators.setOnce保证只接受一个订阅;收到第二个时取消新来的那个。onNext、onError对 null 参数做 NPE 检查,满足 §2.13。
如果你的实现不是 Reactor,这两条要自己写,而且必须写对。下一节就是一个真实的反例。
白盒实测:27 个用例,14 个必测项全过
用 TestNG 7.3.0 运行上面的类:
===============================================
Command line suite
Total tests run: 27, Passes: 14, Failures: 0, Skips: 13
===============================================
13 个跳过全部是 untested_*。它们不是「通过」,是 TCK 1.0.4 没有实现断言。14 个通过项如下:
| 测试方法 | 规则 |
|---|---|
required_exerciseWhiteboxHappyPath | 综合路径:订阅、请求、取消 |
required_spec201_mustSignalDemandViaSubscriptionRequest | §2.1 必须通过 request 表达需求 |
required_spec203_mustNotCallMethodsOnSubscriptionOrPublisherInOnComplete | §2.3 完成回调里不得再 request/cancel |
required_spec203_mustNotCallMethodsOnSubscriptionOrPublisherInOnError | §2.3 错误回调同样不得再操作订阅 |
required_spec205_mustCallSubscriptionCancelIfItAlreadyHasAnSubscriptionAndReceivesAnotherOnSubscribeSignal | §2.5 重复 onSubscribe 要取消新订阅 |
required_spec208_mustBePreparedToReceiveOnNextSignalsAfterHavingCalledSubscriptionCancel | §2.8 取消后仍可能收到已请求的 onNext |
required_spec209_mustBePreparedToReceiveAnOnCompleteSignalWithPrecedingRequestCall | §2.9 先请求,再收到完成 |
required_spec209_mustBePreparedToReceiveAnOnCompleteSignalWithoutPrecedingRequestCall | §2.9 没有请求也要能处理完成 |
required_spec210_mustBePreparedToReceiveAnOnErrorSignalWithPrecedingRequestCall | §2.10 先请求,再收到错误 |
required_spec210_mustBePreparedToReceiveAnOnErrorSignalWithoutPrecedingRequestCall | §2.10 没有请求也要能处理错误 |
required_spec213_onSubscribe_mustThrowNullPointerExceptionWhenParametersAreNull | §2.13 onSubscribe(null) 抛 NPE |
required_spec213_onNext_mustThrowNullPointerExceptionWhenParametersAreNull | §2.13 onNext(null) 抛 NPE |
required_spec213_onError_mustThrowNullPointerExceptionWhenParametersAreNull | §2.13 onError(null) 抛 NPE |
required_spec308_requestMustRegisterGivenNumberElementsToBeProduced | §3.8 请求量必须被记录 |
注意 §2.9、§2.10 各有两条:带前置请求和不带前置请求是两种路径。只处理其中一种,另一种会失败。
spec205:取消的是新订阅
这是白盒最反直觉的一条。规范 §2.5 的原文是:
A Subscriber MUST call
Subscription.cancel()on the givenSubscriptionafter anonSubscribesignal if it already has an activeSubscription.
「given Subscription」指刚收到的那个,不是手里已有的那个。很多人凭直觉写成:
// 错:取消旧订阅,保留新订阅
@Override
public void onSubscribe(Subscription s) {
if (this.subscription != null) {
this.subscription.cancel();
}
this.subscription = s;
s.request(Long.MAX_VALUE);
}
TCK 的报错是:
java.lang.AssertionError: Expected 2nd Subscription given to subscriber to be cancelled,
but Subscription.cancel() was not called within 5000 ms
正确写法是取消新来的重复订阅,保留原来的:
// 对:拒绝重复订阅
@Override
public void onSubscribe(Subscription s) {
if (s == null) {
throw new NullPointerException();
}
if (this.subscription != null) {
s.cancel();
return;
}
this.subscription = s;
s.request(Long.MAX_VALUE);
}
Reactor 的 Operators.setOnce 就是这么做的:已有订阅时直接 cancel() 新传入的订阅,并报告一次重复设置。所以 BaseSubscriber 能过这条,手写状态机反而容易写反。
从协议角度看也合理:一个 Subscriber 只应该有一个活跃订阅。第二个 onSubscribe 是异常信号,拒绝它比替换旧连接更安全。
黑盒:没有 probe,也没有默认请求
黑盒的抽象方法同样是两个:
public abstract Subscriber<T> createSubscriber();
public abstract T createElement(int element);
publisherExecutorService() 也有默认实现。真正的区别是 triggerRequest(Subscriber):
public void triggerRequest(Subscriber<? super T> subscriber) {
// 默认什么都不做
}
默认实现是空的。这意味着如果你的 Subscriber 不在 onSubscribe 里主动请求,而是在外部调用 triggerRequest 时才请求,就必须覆写这个方法;否则 TCK 发不出元素,测试会超时。反过来,像 BaseSubscriber 这样在 hookOnSubscribe 里请求 Long.MAX_VALUE 的实现,不需要覆写。
用同一个 BaseSubscriber 跑黑盒:
Total tests run: 26, Passes: 11, Failures: 0, Skips: 15
11 个必测项是:spec201、spec203(两条)、spec205、spec209(两条)、spec210(两条)、spec213(三条)。15 个跳过全部是 untested_*。
黑盒的 required_spec205_... 名字里带 blackbox,验证的行为与白盒一致:第二个 onSubscribe 到达时,新订阅必须被取消。BaseSubscriber 在两个基类下都能过,说明它实现的是协议本身,不是针对某个测试的特例。
IdentityProcessor:能编译不等于合规
IdentityProcessorVerification<T> 同时验证 Publisher 侧和 Subscriber 侧,必须实现四个方法:
public abstract Processor<T, T> createIdentityProcessor(int bufferSize);
public abstract Publisher<T> createFailedPublisher();
public abstract T createElement(int element); // 来自 WithHelperPublisher
public abstract ExecutorService publisherExecutorService(); // 来自 WithHelperPublisher
createPublisher(long) 有默认实现,内部用 createHelperPublisher(long) 生成元素再喂给被测 Processor,不需要自己写。
「Identity」的含义是:喂进去什么元素,下游就应该按请求量收到什么元素。bufferSize 是 TCK 期望处理器内部能缓冲的容量。一个只做转发、不缓冲的 Processor 不满足这个模型。
拿 RxJava 的 PublishProcessor 试:
@Override
public Processor<String, String> createIdentityProcessor(int bufferSize) {
return PublishProcessor.<String>create().toSerialized();
}
实测结果是:
Total tests run: 68, Passes: 30, Failures: 14, Skips: 24
失败项包括:
| 失败方法 | 现象 |
|---|---|
required_exerciseWhiteboxHappyPath | Did not receive expected cancelling of upstream subscription within 5000 ms |
required_spec101_subscriptionRequestMustResultInTheCorrectNumberOfProducedElements | produced no element after first request within 5000 ms |
required_mustRequestFromUpstreamForElementsThatHaveBeenRequestedLongAgo | 早先请求的元素没有被补发 |
required_spec312_cancelMustMakeThePublisherToEventuallyStopSignaling | 取消后没有停止向上游请求 |
required_spec313_cancelMustMakeThePublisherEventuallyDropAllReferencesToTheSubscriber | 取消后仍持有订阅者引用 |
这些失败不是「RxJava 不合规」。PublishProcessor 是热的多播处理器:没有下游时元素直接丢弃,它本来就不承诺「先收下、等下游请求再发」。把它当作 identity processor 用,语义就不匹配。需要的是带缓冲、按请求量排空的处理器,而不是热转发器。
这也解释了为什么 Processor 的 TCK 最难通过:它要求同一个对象在上游是守规矩的 Subscriber,在下游是守规矩的 Publisher,中间还要把请求量正确传递。只测一侧的实现,往往在另一侧暴露问题。
选择建议
| 场景 | 建议 |
|---|---|
| 自己实现 Subscriber,想验证请求/取消细节 | 白盒,probe 能精确驱动 |
| Subscriber 只是转发,不想暴露内部状态 | 黑盒,成本更低 |
用了 Reactor 的 BaseSubscriber | 两个基类都可以直接跑 |
| 实现 Processor | 必须跑 IdentityProcessorVerification,不能只跑 Publisher 侧 |
| 看到 13/15 个跳过 | 那是 untested_*,不是通过 |
小结
| 关注点 | 结论 |
|---|---|
| 白盒抽象方法 | createSubscriber(probe)、createElement(int);执行器有默认实现 |
| probe 回调 | 四个都要调用,否则测试超时 |
| spec205 | 取消新收到的重复订阅,不是旧订阅 |
| 黑盒 | triggerRequest 默认空实现,惰性请求的实现必须覆写 |
| 白盒实测 | 27 个用例,14 通过、13 个 untested_* |
| 黑盒实测 | 26 个用例,11 通过、15 个 untested_* |
| Processor | 四个抽象方法;热处理器实测 14 个必测项失败 |
阅读导航
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。