Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Async method deadlocks with TestScheduler in ReactiveUI

Tags:

c#

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.

like image 400
thumbmunkeys Avatar asked Dec 11 '18 16:12

thumbmunkeys


2 Answers

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:

  • Answer by Paul Betts
  • Control Time with the TestScheduler

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:

  • Kent Boogaert's Answer
  • Testing Rx code - ISchedulerProvider

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.

like image 57
Funk Avatar answered Nov 04 '22 16:11

Funk


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();
        }
like image 1
MistyK Avatar answered Nov 04 '22 15:11

MistyK