You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 04:25:38