Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unit testing ViewModel property bound to ReactiveCommand IsExecuting

I'm new to ReactiveUI and am following the example set out here, and unit testing as I go.

As expected, the sample code works perfectly, but my unit test which asserts that the SpinnerVisibility property changes as expected when the IsExecuting property of my ReactiveCommand changes, does not.

As per the sample, I have properties on my view model for a spinner visibility and a command to execute a search:

public Visibility SpinnerVisibility => _spinnerVisibility.Value;

public ReactiveCommand<string, List<FlickrPhoto>> ExecuteSearch { get; protected set; }

And in the view model constructor I set up the ExecuteSearch command and SpinnerVisibility is set to change when the command is executing:

public AppViewModel(IGetPhotos photosProvider)
{
    ExecuteSearch = ReactiveCommand.CreateFromTask<string, List<FlickrPhoto>>(photosProvider.FromFlickr);

    this.WhenAnyValue(search => search.SearchTerm)
        .Throttle(TimeSpan.FromMilliseconds(800), RxApp.MainThreadScheduler)
        .Select(searchTerm => searchTerm?.Trim())
        .DistinctUntilChanged()
        .Where(searchTerm => !string.IsNullOrWhiteSpace(searchTerm))
        .InvokeCommand(ExecuteSearch);

    _spinnerVisibility = ExecuteSearch.IsExecuting
        .Select(state => state ? Visibility.Visible : Visibility.Collapsed)
        .ToProperty(this, model => model.SpinnerVisibility, Visibility.Hidden);
}

My initial attempt was to directly invoke the command:

[Test]
public void SpinnerVisibility_ShouldChangeWhenCommandIsExecuting()
{
    var photosProvider = A.Fake<IGetPhotos>();
    var fixture = new AppViewModel(photosProvider);

    fixture.ExecuteSearch.Execute().Subscribe(_ =>
    {
        fixture.SpinnerVisibility.Should().Be(Visibility.Visible);
    });

    fixture.SpinnerVisibility.Should().Be(Visibility.Collapsed);
}

This did result in the state => state ? Visibility.Visible : Visibility.Collapsed lambda being executed, but the subsequent assertion fails as for some reason SpinnerVisibility is still Collapsed.

My next attempt was to indirectly invoke the command by emulating a search using TestScheduler:

[Test]
public void SpinnerVisibility_ShouldChangeWhenCommandIsExecuting()
{
    new TestScheduler().With(scheduler =>
    {
        var photosProvider = A.Fake<IGetPhotos>();
        var fixture = new AppViewModel(photosProvider);

        A.CallTo(() => photosProvider.FromFlickr(A<string>.Ignored)).ReturnsLazily(
            () => new List<FlickrPhoto> { new FlickrPhoto { Description = "a thing", Title = "Thing", Url = "https://thing.com" } });

        fixture.SearchTerm = "foo";
        scheduler.AdvanceByMs(801); // search is throttled by 800ms
        fixture.SpinnerVisibility.Should().Be(Visibility.Visible);
    });
}

As before, the lambda executes, state is true but then instantly re-executes, with state back to false, presumably because, being mocked, photosProvider.FromFlickr would return instantly (unlike retrieving images from the API normally), which would then mean the command was no longer executing.

I then came across Paul Bett's response to a similar question, and added an Observable.Interval to my mock:

A.CallTo(() => photosProvider.FromFlickr(A<string>.Ignored)).ReturnsLazily(
                    () =>
                    {
                        Observable.Interval(TimeSpan.FromMilliseconds(500), scheduler);
                        return new List<FlickrPhoto> {new FlickrPhoto {Description = "a thing", Title = "Thing", Url = "https://thing.com"}};
                    });

and the corresponding test changes:

scheduler.AdvanceByMs(501);
fixture.SpinnerVisibility.Should().Be(Visibility.Collapsed);

This had no effect.

Finally, I awaited the Interval:

A.CallTo(() => photosProvider.FromFlickr(A<string>.Ignored)).ReturnsLazily(async
                    () =>
                    {
                        await Observable.Interval(TimeSpan.FromMilliseconds(500), scheduler);
                        return new List<FlickrPhoto> {new FlickrPhoto {Description = "a thing", Title = "Thing", Url = "https://thing.com"}};
                    });

This allowed the fixture.SpinnerVisibility.Should().Be(Visibility.Visible) assertion to pass, but now regardless how far I advance the scheduler, the mocked method never seems to return and so the subsequent assertion fails.

Is this approach using TestScheduler correct/advised? If so, what am I missing? If not, how should this type of behaviour be tested?

like image 349
applepies Avatar asked Aug 02 '26 23:08

applepies


1 Answers

First off, you are trying to test two independent things in one test. Separating the logic into more focused tests will cause you fewer headaches in the future when refactoring. Consider the following instead:

  1. SearchTerm_InvokesExecuteSearchAfterThrottle
  2. SpinnerVisibility_VisibleWhenExecuteSearchIsExecuting

Now you have unit tests that are verifying each piece of functionality individually. If one fails, you'll know exactly which expectation is broken because there is only one. Now, onto the actual tests...

Based on your code, I assume you're using NUnit, FakeItEasy, and Microsoft.Reactive.Testing. The recommended strategy for testing observables is to use the TestScheduler and assert the final outcome of the observable.

Here is how I would implement them:

using FakeItEasy;
using Microsoft.Reactive.Testing;
using NUnit.Framework;
using ReactiveUI;
using ReactiveUI.Testing;
using System;
using System.Reactive.Concurrency;

...

public sealed class AppViewModelTest : ReactiveTest
{
    [Test]
    public void SearchTerm_InvokesExecuteSearchAfterThrottle()
    {
        new TestScheduler().With(scheduler =>
        {
            var sut = new AppViewModel(A.Dummy<IGetPhotos>());

            scheduler.Schedule(() => sut.SearchTerm = "A");
            scheduler.Schedule(TimeSpan.FromTicks(200), () => sut.SearchTerm += "B");
            scheduler.Schedule(TimeSpan.FromTicks(300), () => sut.SearchTerm += "C");
            scheduler.Schedule(TimeSpan.FromTicks(400), () => sut.SearchTerm += "D");
            var results = scheduler.Start(
                () => sut.ExecuteSearch.IsExecuting,
                0, 100, TimeSpan.FromMilliseconds(800).Ticks + 402);

            results.Messages.AssertEqual(
                OnNext(100, false),
                OnNext(TimeSpan.FromMilliseconds(800).Ticks + 401, true)
            );
        });
    }

    [Test]
    public void SpinnerVisibility_VisibleWhenExecuteSearchIsExecuting()
    {
        new TestScheduler().With(scheduler =>
        {
            var sut = new AppViewModel(A.Dummy<IGetPhotos>());

            scheduler.Schedule(TimeSpan.FromTicks(300),
                () => sut.ExecuteSearch.Execute().Subscribe());
            var results = scheduler.Start(
                () => sut.WhenAnyValue(x => x.SpinnerVisibility));

            results.Messages.AssertEqual(
                OnNext(200, Visibility.Collapsed),
                OnNext(301, Visibility.Visible),
                OnNext(303, Visibility.Collapsed));
        });
    }
}

Notice there is no need to even fake/mock IGetPhotos because your tests aren't verifying anything based on the duration of the command. They just care about when it executes.

Some things can be difficult to wrap your head around at first, such as when a tick actually occurs, but it's very powerful once you get the hang of it. Some debate could be had about the usage of ReactiveUI in the tests (e.g. IsExecuting, WhenAnyValue), but I think it keeps things succinct. Plus, you're using ReactiveUI in your application anyway so if those things broke your test I'd consider that a good thing.

like image 117
Taylor Buchanan Avatar answered Aug 04 '26 14:08

Taylor Buchanan



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!