ReactiveUI中使用SelectMany调用异步方法的测试失败原因
问题:ReactiveUI异步方法集成测试时序异常
尝试为调用模型Foo异步方法的ViewModel编写集成测试,但测试失败——myFoo.Value实际值为0而非预期的123。简化后的代码如下:
using System.Reactive; using Microsoft.Reactive.Testing; using ReactiveUI.Testing; using ReactiveUI.Fody.Helpers; using ReactiveUI; using System.Reactive.Linq; using System.Reactive.Concurrency; namespace Cooler.Test; public class Foo { public int Value { get; private set; } public Foo() => Value = 42; public async Task<Unit> SetValueAsync(int value) { await RxApp.TaskpoolScheduler.Sleep(TimeSpan.FromMilliseconds(10)); Value = value; return Unit.Default; } } public class ViewModel : ReactiveObject { public ViewModel(Foo foo) { this.WhenAnyValue(x => x.Setpoint) //.Skip(1) // Skip the initial value .ObserveOn(RxApp.MainThreadScheduler) .SelectMany(foo.SetValueAsync) .Subscribe(); } [Reactive] public int Setpoint { get; set; } } public class Test { [Fact] public void ShouldCallAsyncMethodOnSettingReactiveSetpoint() { new TestScheduler().With(scheduler => { //set var myFoo = new Foo(); var myVm = new ViewModel(myFoo); //act scheduler.AdvanceBy(1); //process the initial value if not skipped scheduler.AdvanceByMs(20); //async processing myVm.Setpoint = 123; scheduler.AdvanceBy(2); //process reactive scheduler.AdvanceByMs(20); //I expect it to process setpoint setting //assert Assert.Equal(123, myFoo.Value); }); } }
测试中foo.SetValueAsync仅被初始值0触发调用,启用Skip(1)后值为42(符合初始逻辑),但设置Setpoint=123始终不生效。
原因分析
- 全局调度器未替换为测试调度器:代码中
RxApp.MainThreadScheduler和RxApp.TaskpoolScheduler用的是ReactiveUI默认的真实线程调度器,和测试用的TestScheduler完全独立。测试调度器的时间推进无法控制真实线程的异步操作,导致SetValueAsync里的延迟和值更新逻辑不在测试时序内执行。 ObserveOn导致事件无法被测试调度器处理:WhenAnyValue发出的事件被切换到RxApp.MainThreadScheduler(真实主线程),测试调度器的AdvanceBy无法触发这些事件的订阅逻辑,自然不会调用SetValueAsync处理新的Setpoint值。- 时序推进逻辑错误:设置
Setpoint后,没有先触发Rx事件的处理流程,直接推进时间,导致事件根本没被订阅端接收。
解决办法
1. 替换全局调度器为测试调度器
在测试初始化时,把RxApp的全局调度器替换为当前的TestScheduler,让所有Rx操作都受测试调度器控制。
2. 调整时序推进步骤
设置Setpoint后,先推进调度器处理Rx事件,再推进足够时间覆盖异步延迟。
修改后的完整测试代码
[Fact] public void ShouldCallAsyncMethodOnSettingReactiveSetpoint() { new TestScheduler().With(scheduler => { // 将全局调度器替换为测试调度器 RxApp.MainThreadScheduler = scheduler; RxApp.TaskpoolScheduler = scheduler; // 初始化 var myFoo = new Foo(); var myVm = new ViewModel(myFoo); // 操作:设置新的Setpoint值 myVm.Setpoint = 123; // 先推进调度器处理Setpoint变化触发的Rx事件 scheduler.AdvanceBy(1); // 推进足够时间,覆盖SetValueAsync里的10ms延迟 scheduler.AdvanceByMs(10); // 断言 Assert.Equal(123, myFoo.Value); }); }
可选:处理初始值
如果不需要Setpoint的默认初始值(0)触发调用,启用Skip(1)即可:
this.WhenAnyValue(x => x.Setpoint) .Skip(1) // 跳过初始默认值0 .ObserveOn(RxApp.MainThreadScheduler) .SelectMany(foo.SetValueAsync) .Subscribe();
内容的提问来源于stack exchange,提问作者ArjenHiemstra
相关产品推荐
相关产品推荐

