ReactiveUI中的TestScheduler异步方法死锁

时间:2018-12-11 16:55:29

标签: c# reactiveui

我正在尝试在测试中将reactui测试计划程序与异步方法一起使用。

等待异步调用时,测试将挂起。

根本原因似乎是异步方法中等待的命令。

    [Fact]
    public async Task Test()
        => await new TestScheduler().With(async scheduler =>
        {
            await SomeAsyncMethod();

            // *** execution never gets here
            Debugger.Break();
        });

    private async Task SomeAsyncMethod()
    {
        var command = ReactiveCommand.CreateFromTask(async () =>
        {
            await Task.Delay(100);
        });

        // *** this hangs
        await command.Execute();
    }

如何与不会死锁的测试计划程序结合使用异步调用?

我正在使用reactui 9.4.1

编辑:

我已经按照Funks答案中的建议尝试过WithAsync()方法,但是行为是相同的。

3 个答案:

答案 0 :(得分:3)

  

如何与测试计划程序一起进行异步调用?

简而言之

command.Execute()很冷。您需要订阅它,而不是使用await

鉴于您对TestScheduler感兴趣,我认为您想测试一些涉及时间的事物。但是,从When should I care about scheduling部分开始:

  

通过“ new Thread()”或“ Task.Run”创建的线程无法在单元测试中进行控制。

因此,例如,如果您要检查Task是否在100毫秒内完成,则必须等待直到异步方法完成。可以肯定的是,这不是测试TestScheduler的目的。

稍长的版本

TestScheduler的目的是通过使事物运转并在某些时间点验证状态来验证工作流程。由于我们只能在TestScheduler上操作时间,因此,由于无法快速转发实际的计算或I / O,因此通常不希望等待真正的异步代码完成。请记住,这是关于验证工作流的:vm.A在20毫秒时具有新值,因此vm.B应该在120毫秒时具有新值,...

那么如何测试SUT?

1 \您可以使用scheduler.CreateColdObservable

模拟异步方法
public class ViewModelTests
{
    [Fact]
    public void Test()
    {
        string observed = "";

        new TestScheduler().With(scheduler =>
        {
            var observable = scheduler.CreateColdObservable(
                scheduler.OnNextAt(100, "Done"));

            observable.Subscribe(value => observed = value);
            Assert.Equal("", observed);

            scheduler.AdvanceByMs(99);
            Assert.Equal("", observed);

            scheduler.AdvanceByMs(1);
            Assert.Equal("Done", observed);
        });
    }
}

在这里,我们基本上将command.Execute()替换为在var observable上创建的scheduler

很明显,上面的示例非常简单,但是通过多个可观察对象的相互通知,这种测试可以提供有价值的见解以及重构时的安全网。

参考:

2 \您可以明确引用IScheduler

a)使用RxApp

提供的调度程序
public class MyViewModel : ReactiveObject
{
    public string Observed { get; set; }

    public MyViewModel()
    {
        Observed = "";

        this.MyCommand = ReactiveCommand
            .CreateFromTask(SomeAsyncMethod);
    }

    public ReactiveCommand<Unit, Unit> MyCommand { get; }

    private async Task SomeAsyncMethod()
    {
        await RxApp.TaskpoolScheduler.Sleep(TimeSpan.FromMilliseconds(100));
        Observed = "Done";
    }
}

public class ViewModelTests
{
    [Fact]
    public void Test()
    {
        new TestScheduler().With(scheduler =>
        {
            var vm = new MyViewModel();

            vm.MyCommand.Execute().Subscribe();
            Assert.Equal("", vm.Observed);

            scheduler.AdvanceByMs(99);
            Assert.Equal("", vm.Observed);

            scheduler.AdvanceByMs(1);
            Assert.Equal("Done", vm.Observed);
        });
    }
}

注意

  • CreateFromTask创建具有异步执行逻辑的ReactiveCommand。无需将Test方法定义为异步或等待TestScheduler

  • With扩展方法的范围内RxApp.TaskpoolScheduler = RxApp.MainThreadScheduler = new TestScheduler()

b)通过构造函数注入管理自己的调度程序

public class MyViewModel : ReactiveObject
{
    private readonly IScheduler _taskpoolScheduler;
    public string Observed { get; set; }

    public MyViewModel(IScheduler scheduler)
    {
        _taskpoolScheduler = scheduler;
        Observed = "";

        this.MyCommand = ReactiveCommand
            .CreateFromTask(SomeAsyncMethod);
    }

    public ReactiveCommand<Unit, Unit> MyCommand { get; }

    private async Task SomeAsyncMethod()
    {
        await _taskpoolScheduler.Sleep(TimeSpan.FromMilliseconds(100));
        Observed = "Done";
    }
}

public class ViewModelTests
{
    [Fact]
    public void Test()
    {
        new TestScheduler().With(scheduler =>
        {
            var vm = new MyViewModel(scheduler); ;

            vm.MyCommand.Execute().Subscribe();
            Assert.Equal("", vm.Observed);

            scheduler.AdvanceByMs(99);
            Assert.Equal("", vm.Observed);

            scheduler.AdvanceByMs(0);
            Assert.Equal("Done", vm.Observed);
        });
    }
}

参考:

与Haacked的另一句名言让我们保持密切联系:

  

不幸的是,这一点很重要,TestScheduler并没有延伸到现实生活中,因此您的恶作剧仅限于异步Reactive代码。因此,如果您在测试中调用Thread.Sleep(1000),则该线程将真正阻塞一秒钟。但是就测试调度程序而言,还没有时间。

答案 1 :(得分:1)

调用嵌套方法时是否尝试过使用ConfigureAwait(false)?

 [Fact]
    public async Task Test()
        => await new TestScheduler().With(async scheduler =>
        {
            // this hangs
            await SomeAsyncMethod().ConfigureAwait(false);

            // ***** execution will never get to here
            Debugger.Break();
        }

答案 2 :(得分:0)

请尝试在所有异步方法上使用[Fact] public async Task Test() => await new TestScheduler().With(async scheduler => { await SomeAsyncMethod().ConfigureAwait(false); // *** execution never gets here Debugger.Break(); }).ConfigureAwait(false); private async Task SomeAsyncMethod() { var command = ReactiveCommand.CreateFromTask(async () => { await Task.Delay(100).ConfigureAwait(false); }).ConfigureAwait(false); // *** this hangs await command.Execute(); } 。 这将为您提供非阻塞行为。

%252F

测试问题是否与ConfigureAwait相关的另一种方法是将项目移植到Asp.Net Core并在那里进行测试。

Asp.net核心不需要使用ConfigureAwait来防止此阻止问题。

Check this for Reference