Table of Contents

ReactiveUI includes a few tools to help testing, built on what Reactive Extensions for .NET already include. The utilities are included in the ReactiveUI.Testing NuGet package. Make sure to install it into your unit tests project.

Custom Scheduler

Scheduling, and therefore threading, is generally avoided in test scenarios as it can introduce race conditions which may lead to non-deterministic tests — Intro to Rx @ Testing Rx

By default, ReactiveCommand uses RxApp.MainThreadScheduler and ObservableAsPropertyHelper uses CurrentThreadScheduler.Instance, but this behavior can be easily overriden:

// Inject custom schedulers into your view model.
public LoginViewModel(IScheduler customMainThreadScheduler = null)
{
  // If custom main thread scheduler isn't initialized, fallback to default one.
  // For current thread scheduler, use CurrentThreadScheduler.Instance;
  // For task pool scheduler, use RxApp.TaskPoolScheduler.
  customMainThreadScheduler = customMainThreadScheduler ?? RxApp.MainThreadScheduler;

  // Initialize a reactive command with a custom output scheduler.
  _loginCommand = ReactiveCommand.CreateFromTask(
    async () => { /* login logic */ },
    outputScheduler: customMainThreadScheduler
  );

  // Initialize an OAPH with a custom output scheduler.
  _errorMessage = _loginCommand.ThrownExceptions
    .Select(exception => exception.Message)
    .ToProperty(this, x => x.ErrorMessage, 
      scheduler: customMainThreadScheduler);
}

If the view model needs multiple schedulers, you can use the ISchedulerProvider pattern described at Testing Reactive Extensions page:

public interface ISchedulerProvider
{
  IScheduler MainThread { get; }
  IScheduler CurrentThread { get; } 
  IScheduler TaskPool { get; } 
}

Unit Tests

Then, in unit tests project, you can inject a TestScheduler instance that allows you to play with time. There are a few more utility classes to help handle testing, see details on the API documentation site.

new TestScheduler().With(scheduler =>
{
  // Initialize a new view model using the TestScheduler.
  var model = new LoginViewModel(scheduler);
  
  // Play with time.
  scheduler.AdvanceByMs(2 * 100);
});

Replacing Schedulers Without Injecting Them

The With method also replaces the schedulers ReactiveUI is using. This means, that both RxApp.MainThreadScheduler and RxApp.TaskPoolScheduler will stay replaced with TestScheduler until the With method returns.

ReactiveCommands use RxApp.MainThreadScheduler as an output scheduler by default, but OAPHs don't. OAPHs use CurrentThreadScheduler.Instance, which won't get replaced during test execution.

Just a judgement call for performance. It’s assumed that more often than not the observable that pipes into ToProperty is already ticking on the main thread, so scheduling work on the main thread is superfluous and degrades performance. — Kent Boogaart @ You, I and ReactiveUI

That's why in some cases you may need to replace the default scheduler with the RxApp.MainThreadScheduler by hand to handle unit testing.

_errorMessage = _loginCommand.ThrownExceptions
  .Select(exception => exception.Message)
  .ToProperty(this, x => x.ErrorMessage, 
    scheduler: RxApp.MainThreadScheduler);

Playing With Ticks

If you have an asynchronous scenario in your view model implementation (and you do, for sure), you can use .AdvanceBy method to play with ticks. See AdvanceBy docs for details. The example below provides a demo view model code and a unit test for it.

public sealed class LoginViewModel 
{
  private readonly ObservableAsPropertyHelper<bool> _isBusy;
  
  public LoginViewModel(
    IScheduler currentThread,
    IScheduler mainThread,
    IProvider provider)
  {
    // Create a command using an injected scheduler.
    Login = ReactiveCommand.CreateFromTask(
      () => provider.OAuth(), 
      outputScheduler: mainThread);

    // Create an OAPH using an injected scheduler.
    _isBusy = Login.IsExecuting
      .ToProperty(this, x => x.IsBusy, scheduler: currentThread);
  }
  
  public bool IsBusy => _isBusy.Value;
 
  public ReactiveCommand<Unit, Unit> Login { get; }
}

The example below uses NSubstitute to generate a stub for IProvider and FluentAssertions to check view model state.

[Fact]
public void ShouldBeBusyWhenLoggingIn() => new TestScheduler().With(scheduler =>
{
  // Use NSubstute to generate stubs and mocks.
  var provider = Substitute.For<IProvider>();
  var model = new LoginViewModel(scheduler, scheduler, provider);
  
  // IsBusy indicator should be false on init.
  model.IsBusy.Should().BeFalse();
  
  // A test needs to subscribe to a ReactiveCommand,
  // because the execution is lazy and won't trigger
  // with no subscribers.
  model.Login.Execute().Subscribe();

  // We advance our scheduler with two ticks.
  // On the first tick, IsExecuting emits a new value,
  // and the second tick invokes IsBusy.
  scheduler.AdvanceBy(2);    
  model.IsBusy.Should().BeTrue();
  
  // We advance our scheduler with two ticks.
  // On the first tick, IsExecuting emits a new value,
  // and the second tick invokes IsBusy. 
  scheduler.AdvanceBy(2);
  model.IsBusy.Should().BeFalse();
});