Async method deadlocks with TestScheduler in ReactiveUI
Asked Answered
C

3

13

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.

Cherlynchernow answered 11/12, 2018 at 16:55 Comment(9)
Just for troubleshooting, what happens when SomeAsyncMethod just returns a completed task? Trying to narrow down the problem scope (divide and conquer)Accordingly
@Accordingly if I change the method to be not async to return a completed task, then I can't await the execution of the commandCherlynchernow
No. Forget the command for now. I was referring to private Task SomeAsyncMethod() { return Task.CompletedTask; } to see if it would still hang as implied in the first snippet.Accordingly
@Accordingly ok I see - it only hangs when I await the command, just awaiting Task.Delay(100) doesn't hangCherlynchernow
Ok so acknowledging WithAsync is the preferred approach, (as you are attempting to work with async), we now need to check ReactiveCommand.CreateFromTask to see if it is making any blocking calls internally that would cause a deadlockAccordingly
@Accordingly well the command creation doesn't hang, just the part when the command execution gets awaited. Even if the command body itself has no awaited code the command execution will hangCherlynchernow
Ok will need to review the source to confirm if a blocking call is being made github.com/reactiveui/ReactiveUI/blob/…Accordingly
Ok so I created a project to reproduce the problem and was having an issue with it recognizing await command.Execute(). So as a work around I just did not await the command and instead returned a completed task and ran the test, which worked. Not sure if this is a false positive because of that syntax error I was receiving earlier.Accordingly
@Accordingly the cause of the hang is the awaiting of the executed command. I made my question clearer to reflect thatCherlynchernow
S
4

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.

Subtilize answered 16/12, 2018 at 14:40 Comment(1)
@Cherlynchernow After digging a little deeper, I opted to revise my post.Subtilize
S
1

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();
        }
Ssw answered 16/12, 2018 at 14:44 Comment(0)
P
1

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

Palladous answered 23/12, 2018 at 0:38 Comment(4)
this has been pointed out already in another answer, it still hangs unfortunately.Cherlynchernow
Can you please check my answer again?Foreknowledge
it's not working, it has something to do with the reactiveui testschedulerCherlynchernow
Did you tried with .Net core are is it just an assumption? Did you read my edited answer?Foreknowledge

© 2022 - 2024 — McMap. All rights reserved.