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.
SomeAsyncMethod
just returns a completed task? Trying to narrow down the problem scope (divide and conquer) – Accordinglyprivate Task SomeAsyncMethod() { return Task.CompletedTask; }
to see if it would still hang as implied in the first snippet. – AccordinglyTask.Delay(100)
doesn't hang – CherlynchernowWithAsync
is the preferred approach, (as you are attempting to work with async), we now need to checkReactiveCommand.CreateFromTask
to see if it is making any blocking calls internally that would cause a deadlock – Accordinglyawait 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