恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
RxJS v4 测试工具指南:深入理解 `Rx.ReactiveTest` 与虚拟时间断言体系
首页
资讯中心
/
RxJS v4 测试工具指南:深入理解 `Rx.ReactiveTest` 与虚拟时间断言体系
RxJS v4 测试工具指南:深入理解 `Rx.ReactiveTest` 与虚拟时间断言体系
发布时间:2026/9/21 1:36:41
RxJS v4 测试工具指南深入理解Rx.ReactiveTest与虚拟时间断言体系【免费下载链接】RxJSThe Reactive Extensions for JavaScript项目地址: https://gitcode.com/gh_mirrors/rxj/RxJS导读Rx.ReactiveTest是 RxJS v4 测试体系rx.testing.js中的核心工具类为单元测试提供「在指定虚拟时间点构造通知记录」的工厂方法以及三个标准的虚拟时间常量。本文以 ReactiveTest 官方文档 为骨架结合 源码实现、TestScheduler 及其配套的 Recorded / Subscription / MockObserver 等测试组件系统讲解如何用onNext/onError/onCompleted构造时间戳化通知、如何用created/subscribed/disposed控制测试时间轴并给出可直接运行的完整测试用例。读完本文你将掌握 RxJS v4 虚拟时间测试的完整方法论能够独立编写可断言的响应式流测试。一、Rx.ReactiveTest是什么Rx.ReactiveTest是一个静态工具类本质是一个挂载在Rx.ReactiveTest上的对象字面量用于创建用于测试目的的通知记录notification records。它位于rx.testing.js中是Rx.TestScheduler虚拟时间测试体系的基础设施。从源码看Rx.ReactiveTest对象定义了三个字段与四个工厂方法reactivetest.js成员类型说明createdNumber 字段默认创建可观察序列的虚拟时间值恒为100subscribedNumber 字段默认订阅可观察序列的虚拟时间值恒为200disposedNumber 字段默认释放订阅的虚拟时间值恒为1000onNext(ticks, value)方法构造 OnNext 通知记录onError(ticks, exception)方法构造 OnError 通知记录onCompleted(ticks)方法构造 OnCompleted 通知记录subscribe(start, end)方法构造订阅记录Subscription1.1 与 Recorded / Subscription 的关系ReactiveTest工厂方法返回的对象并非普通通知而是携带虚拟时间戳的记录onNext/onError/onCompleted返回 Rx.Recorded 实例内部封装一个 Rx.Notificationsubscribe返回 Rx.Subscription 实例记录订阅与退订时间。这一点在 reactivetest.js 中体现得非常直接onNext: function (ticks, value) { return typeof value function ? new Recorded(ticks, new OnNextPredicate(value)) : new Recorded(ticks, Notification.createOnNext(value)); }, onError: function (ticks, error) { return typeof error function ? new Recorded(ticks, new OnErrorPredicate(error)) : new Recorded(ticks, Notification.createOnError(error)); }, onCompleted: function (ticks) { return new Recorded(ticks, Notification.createOnCompleted()); }, subscribe: function (start, end) { return new Subscription(start, end); }关键点onNext与onError的第二个参数既可以是具体的值/错误对象也可以是一个谓词函数predicate。传入函数时源码会分别包装成OnNextPredicate/OnErrorPredicatereactivetest.js其equals方法会在断言时用该函数对实际值求值从而实现「按条件比较」而非「严格相等比较」。二、类方法详解三个通知工厂2.1Rx.ReactiveTest.onCompleted(ticks)在给定时间创建 OnCompleted 通知记录的工厂方法。参数ticks(Number)—— OnCompleted 通知发生的记录虚拟时间。返回值(Recorded)—— OnCompleted 通知记录。位置rx.testing.js源码见 reactivetest.js示例构造一个在虚拟时间260完成的热可观察序列并断言结果var onCompleted Rx.ReactiveTest.onCompleted; var scheduler new Rx.TestScheduler(); var xs scheduler.createHotObservable( onCompleted(260) ); var res scheduler.startScheduler(function () { return xs.map(function (x) { return x; }); }); // 编写自定义断言 collectionAssert.assertEqual(res.messages, [ onCompleted(260) ]);2.2Rx.ReactiveTest.onError(ticks, exception)在给定时间、携带给定错误创建 OnError 通知记录的工厂方法。参数ticks(Number)—— OnError 通知发生的记录虚拟时间exception(Error | Function)—— 存入 OnError 通知的错误对象或一个谓词函数。返回值(Recorded)—— OnError 通知记录。位置rx.testing.js源码见 reactivetest.js示例用new Error(woops)构造错误断言时既可以直接比较错误对象也可以改用谓词按e.message匹配var ex new Error(woops); var onError Rx.ReactiveTest.onError; var scheduler new Rx.TestScheduler(); var xs scheduler.createHotObservable( onError(201, ex) ); var res scheduler.startScheduler(function () { return xs.map(function (x) { return x; }); }); // 编写自定义断言 collectionAssert.assertEqual(res.messages, [ // 使用谓词 onError(201, function (e) { return e.message woops; }) ]);2.3Rx.ReactiveTest.onNext(ticks, value)在给定时间、携带给定值创建 OnNext 通知记录的工厂方法。参数ticks(Number)—— OnNext 通知发生的记录虚拟时间value(Any | Function)—— 存入 OnNext 通知的值或一个谓词函数。返回值(Recorded)—— OnNext 通知记录。位置rx.testing.js源码见 reactivetest.js示例断言收到值42既可直接比较也可用谓词x 42var onNext Rx.ReactiveTest.onNext; var scheduler new Rx.TestScheduler(); var xs scheduler.createHotObservable( onNext(201, 42) ); var res scheduler.startScheduler(function () { return xs.map(function (x) { return x; }); }); // 编写自定义断言 collectionAssert.assertEqual(res.messages, [ // 使用谓词 onNext(201, function (x) { return x 42; }) ]);设计意图谓词形式的断言在比较引用类型、时间戳或异步错误时尤其有用——断言只关心「这条记录是否满足某个条件」而不必与预期对象严格同引用。这正是OnNextPredicate/OnErrorPredicate的equals实现reactivetest.js所保证的。三、类字段详解三个虚拟时间常量ReactiveTest还提供了三个默认虚拟时间常量它们是 TestScheduler.startScheduler 在未显式指定设置时的默认值字段值语义created100创建可观察序列的默认虚拟时间subscribed200订阅可观察序列的默认虚拟时间disposed1000释放订阅的默认虚拟时间源码中直接定义了这三个常量reactivetest.jscreated: 100, subscribed: 200, disposed: 1000,而startScheduler会在设置缺失时回退到这些值testscheduler.jssettings || (settings {}); settings.created null (settings.created ReactiveTest.created); settings.subscribed null (settings.subscribed ReactiveTest.subscribed); settings.disposed null (settings.disposed ReactiveTest.disposed);3.1created—— 创建时间默认 100默认用于在单元测试中创建可观察序列的虚拟时间。显式传入时通常以位置参数形式出现var scheduler new Rx.TestScheduler(); var xs scheduler.createHotObservable( Rx.ReactiveTest.onNext(201, 42), Rx.ReactiveTest.onNext(202, 56), Rx.ReactiveTest.onCompleted(203) ); var res scheduler.startScheduler( function () { return xs.map(function (x) { return x; })}, Rx.ReactiveTest.created, // 创建时间 Rx.ReactiveTest.subscribed, // 订阅时间 Rx.ReactiveTest.disposed // 释放时间 );3.2subscribed—— 订阅时间默认 200默认用于在单元测试中订阅可观察序列的虚拟时间。它是「热序列 vs 冷序列」行为差异的关键分界点下文 4.1 会详述。3.3disposed—— 释放时间默认 1000默认用于在单元测试中释放订阅的虚拟时间。释放后后续到达的通知不再被记录到res.messages。这三个字段除位置参数形式外还可以封装进一个settings对象传给startSchedulervar res scheduler.startScheduler( function () { return xs.map(function (x) { return x; })}, { created: Rx.ReactiveTest.created, subscribed: Rx.ReactiveTest.subscribed, disposed: Rx.ReactiveTest.disposed } );两种传参方式等价——startScheduler(create, settings)的第二个参数既可传扁平位置参数created, subscribed, disposed也可传{ created, subscribed, disposed }对象见 testscheduler.js。四、实战与 TestScheduler 组合的完整测试流程ReactiveTest的工厂方法本身不执行任何调度它们只负责「造记录」。真正驱动虚拟时间前进的是Rx.TestScheduler——一个继承自Rx.VirtualTimeScheduler的虚拟时间调度器专用于测试基于 Reactive Extensions 构建的应用与库testscheduler.js。4.1 经典完整用例热序列下面的例子是官方文档给出的标准工作流先创建热可观察序列在150发1、210发2、220发3、230完成再以默认时间轴created100、subscribed200、disposed1000启动对结果做map(x x * x)后断言function createMessage(expected, actual) { return Expected: [ expected.toString() ]\r\nActual: [ actual.toString() ]; } // 使用 QUnit 进行断言 var collectionAssert { assertEqual: function (actual, expected) { var comparer Rx.internals.isEqual, isOk true; if (expected.length ! actual.length) { ok(false, Not equal length. Expected: expected.length Actual: actual.length); return; } for(var i 0, len expected.length; i len; i) { isOk comparer(expected[i], actual[i]); if (!isOk) { break; } } ok(isOk, createMessage(expected, actual)); } }; var onNext Rx.ReactiveTest.onNext, onCompleted Rx.ReactiveTest.onCompleted, subscribe Rx.ReactiveTest.subscribe; var scheduler new Rx.TestScheduler(); // 创建热可观察序列订阅后立即开始发射 var xs scheduler.createHotObservable( onNext(150, 1), onNext(210, 2), onNext(220, 3), onCompleted(230) ); // 注意默认在 200 订阅因此会错过 150 时刻的通知 var res scheduler.startScheduler(function () { return xs.map(function (x) { return x * x }); }); // 实现集合断言 collectionAssert.assertEqual(res.messages, [ onNext(210, 4), onNext(220, 9), onCompleted(230) ]); // 检查订阅/退订时间 collectionAssert.assertEqual(xs.subscriptions, [ subscribe(200, 230) ]);这个用例同时展示了三件事通知断言res.messages记录了订阅窗口内收到的全部带时间戳通知订阅断言xs.subscriptions记录了热序列被订阅200与退订230因流在 230 完成的时间时间过滤150时刻的onNext(150, 1)因为发生在订阅时刻200之前而被正确忽略——这是热可观察序列hotobservable.js的固有行为通知按绝对虚拟时间发射订阅者只收到订阅之后的通知。4.2 冷序列偏移语义与热序列相反冷可观察序列createColdObservable的通知时刻是相对订阅时刻的偏移量。其实现coldobservable.js在订阅时才将相对时间换算为绝对时间因此下面例子里onNext(150, 1)实际出现在绝对时间200 150 350之后……不过要注意的是startScheduler默认在200订阅、1000释放冷序列onNext(150,1)、onNext(200,2)、onNext(250,3)、onCompleted(300)会分别落在绝对时间350、400、450、500var onNext Rx.ReactiveTest.onNext, onCompleted Rx.ReactiveTest.onCompleted, subscribe Rx.ReactiveTest.subscribe; var scheduler new Rx.TestScheduler(); // 创建冷可观察序列时刻相对订阅时间偏移 var xs scheduler.createColdObservable( onNext(150, 1), onNext(200, 2), onNext(250, 3), onCompleted(300) ); // 注意默认在 200 订阅 var res scheduler.startScheduler(function () { return xs.filter(function (x) { return x % 2 0; }); }); // 实现集合断言 collectionAssert.assertEqual(res.messages, [ onNext(400, 2), onCompleted(500) ]); // 检查订阅/退订时间 collectionAssert.assertEqual(xs.subscriptions, [ subscribe(200, 500) ]);由于filter只放行偶数1与3被过滤最终在绝对时间400 200 200收到2在500 200 300完成订阅记录为subscribe(200, 500)。冷序列因此特别适合测试「相对时序」与运算符的重排行为。4.3 用createObserver手工控制时间轴当需要更精细地控制「何时订阅、何时退订」时可以用scheduler.createObserver()创建记录型观察者再配合scheduleAbsolute手工编排。MockObservermockobserver.js内部维护一个messages数组每次收到通知都会用当前scheduler.clock打上时间戳var onNext Rx.ReactiveTest.onNext; var scheduler new Rx.TestScheduler(); var d new Rx.SerialDisposable(); var xs Rx.Observable.return(42, scheduler); var res scheduler.createObserver(); scheduler.scheduleAbsolute(null, 100, function () { return d.setDisposable(xs.subscribe( function (x) { d.dispose(); res.onNext(x); }, res.onError.bind(res), res.onCompleted.bind(res) )); }); scheduler.start(); collectionAssert.assertEqual(res.messages, [ onNext(101, 42) ]);Rx.Observable.return(42, scheduler)在虚拟时间100 1 101时刻发射42测试断言精确到单个 tick。4.4 测试 PromisecreateResolvedPromise/createRejectedPromiseTestScheduler 还提供了两个 Promise 工厂testscheduler.js内部用MockPromisemockpromise.js实现它把「resolve/reject」也翻译成带时间戳的虚拟通知createResolvedPromise(ticks, value)→ 在ticks时刻依次发射onNext(ticks, value)与onCompleted(ticks)createRejectedPromise(ticks, reason)→ 在ticks时刻发射onError(ticks, reason)。解析成功示例var onNext Rx.ReactiveTest.onNext, onError Rx.ReactiveTest.onError, onCompleted Rx.ReactiveTest.onCompleted; var scheduler new Rx.TestScheduler(); // 创建已解析的 Promise var xs scheduler.createResolvedPromise(201, 1); // 注意默认在 200 订阅 var res scheduler.startScheduler(function () { // 由于 issue #976需要显式传入测试调度器 return Rx.Observable.fromPromise(xs, scheduler); }); // 实现集合断言 collectionAssert.assertEqual(res.messages, [ onNext(201, 1), onCompleted(201) ]);拒绝示例var scheduler new Rx.TestScheduler(); // 创建被拒绝的 Promise var error new Error(); var xs scheduler.createRejectedPromise(201, error); var res scheduler.startScheduler(function () { // 由于 issue #976需要显式传入测试调度器 return Rx.Observable.fromPromise(xs, scheduler); }); // 实现集合断言 collectionAssert.assertEqual(res.messages, [ onError(201, error) ]);注意官方文档标注的坑调用Rx.Observable.fromPromise时必须把scheduler作为第二参数显式传入参见 issue #976否则 Promise 的调度时机无法被虚拟时间控制。4.5startScheduler的三个默认时刻startScheduler(create, settings)的完整签名与默认值testscheduler.js参数类型默认值语义createFunction—必填创建可观察序列的工厂函数settings.createdNumber100调用create创建序列的时刻settings.subscribedNumber200订阅所创建序列的时刻settings.disposedNumber1000释放订阅的时刻其内部流程如下源码级createObserver()创建记录型观察者在created时刻调度createFn()得到source在subscribed时刻调度source.subscribe(observer)在disposed时刻调度subscription.dispose()调用this.start()推进虚拟时钟直到队列清空返回携带时间戳通知的 observer。返回的 observer 的messages属性Recorded[]即最终断言对象。五、配套断言对象Recorded 与 Subscription 深入理解ReactiveTest工厂返回的对象类型才能写出准确的断言。5.1Rx.Recorded—— 值 虚拟时间Recorded记录「值在哪个虚拟时间产生」recorded.jsvar Recorded Rx.Recorded function (time, value, comparer) { this.time time; this.value value; this.comparer comparer || defaultComparer; };实例属性time产生值的虚拟时间、value产生的值、comparer比较器默认使用Rx.internals.isEqualequals(other)要求time严格相等且comparer(this.value, other.value)为真recorded.jstoString()返回valuetime形式如foo201recorded.js。var r1 new Rx.Recorded(201, foo); var r2 new Rx.Recorded(201, bar); var r3 new Rx.Recorded(201, foo); console.log(r1.equals(r2)); // false console.log(r1.equals(r3)); // true console.log(r1.toString()); // foo201当ReactiveTest.onNext收到谓词函数时value位置保存的其实是一个OnNextPredicate包装对象其equals通过调用谓词完成比较reactivetest.js。5.2Rx.Subscription—— 订阅/退订时间对Subscription记录对可观察序列的订阅与退订subscription.jsvar Subscription Rx.Subscription function (start, end) { this.subscribe start; this.unsubscribe end || Number.MAX_VALUE; };实例属性subscribe订阅虚拟时间、unsubscribe退订虚拟时间缺省为Number.MAX_VALUE即「无限/未退订」equals(other)订阅与退订时间均严格相等才返回truesubscription.jstoString()未退订输出(201, Infinite)已退订输出(201, 1000)subscription.js。var s1 new Rx.Subscription(201, 500); var s2 new Rx.Subscription(201); var s3 new Rx.Subscription(201, 500); console.log(s1.equals(s2)); // false console.log(s1.equals(s3)); // true console.log(s2.toString()); // (201, Infinite)热/冷可观察序列的subscriptions数组正是由Subscription实例构成订阅时记录subscribe时间退订时补全unsubscribe时间见 hotobservable.js。六、在仓库中的实际印证ReactiveTest并非「文档专属」的纸面 API仓库自身的测试套件就大量使用它。以 tests/observable/where.js 为例其头部直接从Rx.ReactiveTest解构出全部工厂方法与常量onNext Rx.ReactiveTest.onNext, onError Rx.ReactiveTest.onError, onCompleted Rx.ReactiveTest.onCompleted, subscribe Rx.ReactiveTest.subscribe, created Rx.ReactiveTest.created, subscribed Rx.ReactiveTest.subscribed, disposed Rx.ReactiveTest.disposed;随后几乎所有测试都遵循同一模式用Rx.TestSchedulercreateHotObservable/createColdObservable构造输入用startScheduler运行被测运算符最后用collectionAssert.assertEqual(res.messages, [...])断言通知序列、用xs.subscriptions断言订阅生命周期。你可以参考 tests/observable 目录下的 130 余个测试文件为每个运算符找到对应的ReactiveTest使用范式。加载与依赖说明ReactiveTest位于rx.testing.js构建产物见 modules/rx-lite-testing/rx.lite.testing.js 与 modules/rx-core-testing/rx.core.testing.js使用前提需先加载rx.js/rx.compat.js/rx.lite.js等任一完整核心库包分发npm 包rx、NuGet 包RxJS-All与RxJS-Testing对应 nuget/RxJS-Testing均包含该模块相关类型定义位于 ts/rx.testing.d.ts可配合 TypeScript 使用。七、小结Rx.ReactiveTest是 RxJS v4 虚拟时间测试的「语法核心」三个工厂方法onNext/onError/onCompleted把「值/错误/完成 虚拟时间」封装成Recorded记录且支持谓词形式的柔性断言三个时间常量created(100)/subscribed(200)/disposed(1000)定义了startScheduler的默认测试时间轴subscribe(start, end)生成Subscription记录用于断言订阅/退订生命周期与Rx.TestScheduler热/冷序列工厂、createObserver、Promise 工厂、startScheduler组合后即可对任意运算符的时序行为做确定性、可重复的单元测试。这套「记录-调度-断言」的测试方法论不仅支撑了本仓库 tests 目录下全部运算符测试也构成了后来 RxJS 各版本虚拟时间测试marble testing的直接前身。掌握ReactiveTest就等于掌握了 RxJS 时序测试的根基。【免费下载链接】RxJSThe Reactive Extensions for JavaScript项目地址: https://gitcode.com/gh_mirrors/rxj/RxJS创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考