Microsoft.Reactive.Testing 实战指南:用虚拟时间测试 Rx.NET 自定义算子
发布时间:2026/9/29 7:29:43来源:尧图网络
后端【免费下载链接】reactiveThe Reactive Extensions for .NET项目地址https://gitcode.com/gh_mirrors/re/reactive点击查看免费下载导读Microsoft.Reactive.Testing是 Rx.NETReactive Extensions for .NET仓库中随包发布的测试工具库它提供了一套基于**虚拟时间Virtual Time**的调度与记录机制让你无需真实等待、也无需依赖线程调度就能精确断言任意 Rx 算子在某条时间轴上的行为。本指南围绕该包的官方说明文档与源码实现讲解其设计定位、核心类型体系、热/冷可观测序列语义、断言方法以及如何把同一套测试范式应用到你自己编写的自定义算子与库中。包的官方定位为内部而生却对自定义算子开放NuGet 包Microsoft.Reactive.Testing的官方说明NuGet.Readme.md用很短的话界定了它的身份该包主要面向 dotnet/reactive 仓库内部使用用于对 Rx 算子做基于虚拟时间的测试因为它能测试任何自定义 Rx 算子对自己实现 Rx 算子的库同样有用这也是它被发布为 NuGet 包的原因但它的使用当前不受官方支持且没有任何向后兼容承诺。这段定位意味着两件事其一你可以放心地把这套工具当作编写 Rx 测试的参照与素材其二把它引入生产代码或长期维护的项目前需要评估 API 随仓库演进发生变化的可能性。从工程配置看Microsoft.Reactive.Testing.csproj该库目标框架为netstandard2.0;net8.0项目引用System.Reactive并依赖System.Collections.Immutable与xunit.v3.assert.source——说明其断言能力构建在 xUnit 之上。同时 csproj 中通过None Includebuild\NuGet.Readme.md Packtrue PackagePath\readme.md /将这份说明作为包内 readme 打包这就是你读到的那份文档的出处。核心类型体系一张图看懂测试组件Microsoft.Reactive.Testing目录下共 13 个源文件构成一套完整的小型测试框架类型职责TestScheduler.cs虚拟时间调度器测试的时钟引擎ReactiveTest.cs测试基类提供消息/订阅的工厂方法与默认时间常量Recorded.cs一条值 虚拟时间的记录Subscription.cs订阅生命周期记录订阅时刻、退订时刻ITestObservable.cs可测试可观测序列接口暴露Subscriptions与MessagesITestObserver.cs可测试观察者接口暴露收到的MessagesHotObservable.cs热序列消息按绝对时间播放与订阅时机无关ColdObservable.cs冷序列消息按相对时间播放从订阅时刻起算MockObserver.cs记录型观察者把收到的通知连同时间戳存起来ReactiveAssert.cs针对序列、通知、异常的断言辅助类测试的基本流程是用TestScheduler创建带时间戳的消息剧本hot/cold 序列→ 在指定虚拟时间点触发创建、订阅、退订 → 用MockObserver即ITestableObserver收集带时间戳的结果 → 与期望的Recorded序列逐条比对。TestScheduler虚拟时钟引擎TestScheduler继承自 Rx 的VirtualTimeSchedulerlong, long用long表示虚拟时间。关键实现TestScheduler.cs时间单位ToRelative返回timeSpan.TicksToDateTimeOffset将绝对虚拟时间直接映射为new DateTimeOffset(absolute, TimeSpan.Zero)——即虚拟时间单位就是 .NET 的 tick100 纳秒与真实时间换算直观。时间加法Add直接做absolute relative。防回拨ScheduleAbsolute被重写若请求的dueTime Clock则强制调整为Clock 1避免调度器把任务排到当前或过去而出现重复执行或时序错乱。TestScheduler的三个StartT重载是测试入口TestScheduler.cs// 全参数版本自定义创建/订阅/退订时间 public ITestableObserverT StartT(FuncIObservableT create, long created, long subscribed, long disposed); // 只指定退订时间创建/订阅使用默认时间 public ITestableObserverT StartT(FuncIObservableT create, long disposed); // 全默认created100, subscribed200, disposed1000 public ITestableObserverT StartT(FuncIObservableT create);Start的底层行为见 TestScheduler.cs是把三个动作分别调度到虚拟时间轴上的三个时刻在created时刻调用工厂函数创建序列source create()在subscribed时刻把创建的序列订阅到测试观察者上source.Subscribe(observer)在disposed时刻退订subscription.Dispose()。随后启动调度器开始拨钟虚拟时间推进过程中触发上述动作及序列本身的消息。Start对非法参数会抛异常create为 null 抛ArgumentNullExceptionsubscribed created或disposed created/subscribed抛ArgumentOutOfRangeException。除Start外调度器还提供三个工厂方法var hot scheduler.CreateHotObservableint(/* RecordedNotificationint[] */); var cold scheduler.CreateColdObservableint(/* RecordedNotificationint[] */); var observer scheduler.CreateObserverint(); // 返回 ITestableObserverintReactiveTest消息与订阅的剧本语言ReactiveTest是测试类的基类也可作为静态成员来源提供了声明测试数据的工厂方法与三个约定常量ReactiveTest.cs常量值含义Created100默认的序列创建虚拟时间Subscribed200默认的订阅虚拟时间Disposed1000默认的退订虚拟时间之所以留出100 → 200 → 1000的间隔是为了让创建前/订阅前/退订后的事件天然落在不同区间方便验证算子是否在合适的时机才开始/停止监听。消息工厂方法返回RecordedNotificationTOnNextT(long ticks, T value) // 期望在某时刻发出值 OnNextT(long ticks, FuncT, bool predicate) // 期望某时刻发出的值满足谓词 OnCompletedT(long ticks) // 期望某时刻完成 OnErrorT(long ticks, Exception exception) // 期望某时刻抛出指定异常 OnErrorT(long ticks, FuncException, bool predicate) // 期望某时刻抛出的异常满足谓词订阅记录工厂方法Subscribe(long start) // 期望在 start 时刻订阅且未退订 Subscribe(long start, long end) // 期望在 start 订阅、end 退订基于谓词的重载内部实现值得注意OnNextPredicateT与OnErrorPredicateT通过重写NotificationT.Equals实现按条件匹配而非按值相等ReactiveTest.cs因此当被测序列的值类型没有值相等语义如匿名类型、含引用类型的复杂对象时仍然可以精确断言。记录与订阅Recorded 与 SubscriptionRecordedT是readonly struct由Time与Value组成相等性判定为Time other.Time EqualityComparerT.Default.Equals(Value, other.Value)Recorded.cs。它的ToString()输出形如3230值时间断言失败时的诊断信息正是依赖这种可读格式。Subscription同样是readonly struct记录订阅与退订两个虚拟时间并定义常量Infinite long.MaxValue表示从未退订Subscription.cs。当期望某个序列在测试窗口内始终保持订阅时就使用Subscribe(200)退订时间为 Infinite。Hot 与 Cold两种测试序列语义Hot 序列HotObservable.cs构造时就把所有消息按绝对虚拟时间调度到调度器上无论有没有订阅者、何时订阅消息到点就广播。因此订阅发生在消息之后的消息将永远收不到——这正是热序列的本意用于模拟事件总线、鼠标移动等与订阅时机无关的外部事件源。它内部维护_observers列表与_subscriptions列表退订时记录结束时间。Cold 序列ColdObservable.cs构造时只保存消息剧本每次Subscribe时才把每条消息按相对虚拟时间ScheduleRelative重新调度相当于每个订阅者都从零开始看一遍完整序列这正是冷序列的语义如 HTTP 请求、文件读取。_messages中消息的时间是相对订阅时刻的偏移。二者都实现ITestableObservableTITestObservable.cs暴露Subscriptions订阅生命周期列表与Messages剧本消息列表方便断言算子到底在何时订阅、何时退订、收到过哪些输入。测试观察者MockObserverMockObserver.cs则在OnNext/OnError/OnCompleted回调里把通知连同当时的_scheduler.Clock一起记入Messages作为断言结果。ReactiveAssert把期望变成断言ReactiveAssert是静态断言类ReactiveAssert.cs核心方法AreElementsEqualT(expected, actual)同时支持IEnumerableT与IObservableT两个重载族。对IObservable会先用Materialize().ToEnumerable()把通知流OnNext/OnError/OnCompleted物化为可枚举序列再比对因此能同时校验值与终止方式可选的IEqualityComparerT与message参数用于自定义比较与失败信息。AssertEqualT(this IEnumerableT actual, IEnumerableT expected)及 params 数组重载扩展方法形态便于链式断言。ThrowsTException(Action action)断言某动作抛出TException或其子类支持指定异常实例用Assert.Same校验必须是同一个异常对象与自定义消息。断言失败时的诊断信息形如Expected: [3230, 5340, 7390, 11580, OnCompleted600] Actual..: [3230, 5340, 7390, OnCompleted600]实战用同一套范式测试 Where 算子与你的自定义算子仓库真实测试 WhereTest.cs 的Where_Complete用例展示了完整范式[TestMethod] public void Where_Complete() { var scheduler new TestScheduler(); var invoked 0; // 热序列剧本110/180 时刻的消息早于订阅(200)应被忽略 var xs scheduler.CreateHotObservable( OnNext(110, 1), OnNext(180, 2), OnNext(230, 3), OnNext(270, 4), OnNext(340, 5), OnNext(380, 6), OnNext(390, 7), OnNext(450, 8), OnNext(470, 9), OnNext(560, 10), OnNext(580, 11), OnCompletedint(600), OnNext(610, 12), // 完成后不应再收到 OnErrorint(620, new Exception()), OnCompletedint(630) ); var res scheduler.Start(() xs.Where(x { invoked; return IsPrime(x); }) ); res.Messages.AssertEqual( OnNext(230, 3), OnNext(340, 5), OnNext(390, 7), OnNext(580, 11), OnCompletedint(600) ); xs.Subscriptions.AssertEqual(Subscribe(200, 600)); Assert.Equal(9, invoked); // 仅订阅窗口内(200~600)的消息触发过谓词 }这段测试在零真实等待的情况下验证了 Where 算子的三条关键行为过滤逻辑正确、完成通知正确、退订后600 之后的消息被彻底隔离——同时通过Subscriptions断言算子确实只在200 → 600窗口内保持订阅。把这套范式迁移到自定义算子上同样直接[TestClass] public class MyOperatorTests : ReactiveTest { [TestMethod] public void MyOperator_Keeps_Relative_Timing() { var scheduler new TestScheduler(); // 冷序列相对订阅时刻偏移的消息剧本 var xs scheduler.CreateColdObservable( OnNext(10, 1), OnNext(20, 2), OnCompletedint(30) ); var res scheduler.Start(() xs.MyOperator()); res.Messages.AssertEqual( OnNext(210, 1), OnNext(220, 2), OnCompletedint(230) ); xs.Subscriptions.AssertEqual(Subscribe(200, 230)); } }注意冷序列的期望时间要换算成绝对虚拟时间订阅默认在 200加上消息的相对偏移 10/20/30。适用边界与注意事项依据官方文档使用本包前请牢记三个约束不受支持Microsoft.Reactive.Testing主要为 dotnet/reactive 仓库内部使用官方不承诺对外支持无向后兼容承诺API 可能随仓库演进而变化升级包版本时需关注行为差异面向测试它构建于 xUnit 断言之上csproj 引用了xunit.v3.assert.source适合与 xUnit 测试框架搭配若项目使用其他测试框架可直接复用TestScheduler/CreateHotObservable等基础设施仅替换断言层。如果你正在为自定义 Rx 算子编写单元测试可以完全复用本仓库测试套件 Tests.System.Reactive 中数百个算子测试如 WhereTest、SelectTest、MergeTest 等的写法作为参照——它们正是这套虚拟时间测试范式在真实 Rx 算子上的大规模验证。小结Microsoft.Reactive.Testing的价值在于把时间变成可精确操控、可重复验证的输入TestScheduler提供虚拟时钟ReactiveTest提供声明式的消息剧本语言Recorded/Subscription提供可比较的记录值ReactiveAssert提供序列级断言。即便官方明确其内部使用、不支持、不保证兼容的定位这套范式依然是编写确定性 Rx 测试最直接的参照也是理解 Rx 调度模型的一扇窗口。赞分享后端【免费下载链接】reactiveThe Reactive Extensions for .NET项目地址https://gitcode.com/gh_mirrors/re/reactive点击查看免费下载相关推荐sqld 服务器模式完全指南将 libSQL 打造为 HTTP/SQL 数据库服务sqld 服务器模式完全指南将 libSQL 打造为 HTTP/SQL 数据库服务 sqld SQL daemon是 libSQL 项目提供的服务器模式实后端RxJS v4 Rx.VirtualTimeScheduler 虚拟时间调度器从自定义实现到测试实战RxJS v4 Rx.VirtualTimeScheduler 虚拟时间调度器从自定义实现到测试实战 Rx.VirtualTimeScheduler 是 Rx后端Bookshelf.js高级用法虚拟属性、时间戳和自定义方法Bookshelf.js高级用法虚拟属性、时间戳和自定义方法 Bookshelf.js是一个基于Express.js的简单、灵活的Node.js ORM它提后端上一篇Windows Cleaner免费一键C盘清理3分钟腾出空间告别卡顿下一篇Krita AI Diffusion 插件安装后不可见找不到 AI Image Generation 的排查与解决方法创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
网站建设高端定制企业官网