简体   繁体   中英

Async method deadlocks with TestScheduler in ReactiveUI

I'm trying to use the reactiveui test scheduler with an async method in a test.

The test hangs when the async call is awaited.

The root cause seems to be a command that's awaited in the async method.

    [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();
    }

How can I do an async call in combination with the test scheduler that does not deadlock?

I'm using reactiveui 9.4.1

EDIT:

I've tried the WithAsync() method as suggested in Funks answer, but the behaviour is the same.

How can I do an async call in combination with the test scheduler?

In short

command.Execute() is a cold observable. You need to subscribe to it, instead of using await .

Given your interest in TestScheduler , I take it you want to test something involving time. However, from the When should I care about scheduling section:

threads created via "new Thread()" or "Task.Run" can't be controlled in a unit test.

So, if you want to check, for example, if your Task completes within 100ms, you're going to have to wait until the async method completes. To be sure, that's not the kind of test TestScheduler is meant for.

The somewhat longer version

The purpose of TestScheduler is to verify workflows by putting things in motion and verifying state at certain points in time. As we can only manipulate time on a TestScheduler , you'd typically prefer not to wait on real async code to complete, given there's no way to fast forward actual computations or I/O. Remember, it's about verifying workflows: vm.A has new value at 20ms, so vm.B should have new val at 120ms,...

So how can you test the SUT?

1\\ You could mock the async method using 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);
        });
    }
}

Here we basically replaced command.Execute() with var observable created on scheduler .

It's clear the example above is rather simple, but with several observables notifying each other this kind of test can provide valuable insights, as well as a safety net while refactoring.

Ref:

2\\ You could reference the IScheduler explicitly

a) Using the schedulers provided by 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);
        });
    }
}

Note

  • CreateFromTask creates a ReactiveCommand with asynchronous execution logic. There's no need to define the Test method as async or await the TestScheduler .

  • Within the With extension method's scope RxApp.TaskpoolScheduler = RxApp.MainThreadScheduler = the new TestScheduler() .

b) Managing your own schedulers through constructor injection

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);
        });
    }
}

Ref:

Let's close ranks with another quote from Haacked:

Unfortunately, and this next point is important, the TestScheduler doesn't extend into real life, so your shenanigans are limited to your asynchronous Reactive code. Thus, if you call Thread.Sleep(1000) in your test, that thread will really be blocked for a second. But as far as the test scheduler is concerned, no time has passed.

Have you tried to use ConfigureAwait(false) when calling nested method?

 [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();
        }

Please try using .ConfigureAwait(false) on all your async methods. This will provide you non-blocking behavior.

[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();
}

Another way to test whether the problem is related with ConfigureAwait is to port your project to Asp.Net Core and test it there.

Asp.net core does not need to use ConfigureAwait to prevent this blocking issue.

Check this for Reference

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM