Recommended way to test Scheduler/Throttle
Asked Answered
E

1

3

I'm in the process of rewriting one little WPF-App I wrote to make use of ReactiveUI, to get a feeling about the library.

I really like it so far!

Now I've stumbled upon the Throttle method and want to use it when applying a filter to a collection.

This is my ViewModel:

namespace ReactiveUIThrottle
{
    public class MainViewModel : ReactiveObject
    {
        private string _filter;

        public string Filter { get => _filter; set => this.RaiseAndSetIfChanged(ref _filter, value); }

        private readonly ReactiveList<Person> _persons = new ReactiveList<Person>();

        private readonly ObservableAsPropertyHelper<IReactiveDerivedList<Person>> _filteredPersons;

        public IReactiveDerivedList<Person> Persons => _filteredPersons.Value;
        public MainViewModel()
        {
            Filter = string.Empty;
            _persons.AddRange(new[]
            {
                new Person("Peter"),
                new Person("Jane"),
                new Person("Jon"),
                new Person("Marc"),
                new Person("Heinz")
            });

            var filterPersonsCommand = ReactiveCommand.CreateFromTask<string, IReactiveDerivedList<Person>>(FilterPersons);

            this.WhenAnyValue(x => x.Filter)
                // to see the problem
                .Throttle(TimeSpan.FromMilliseconds(2000), RxApp.MainThreadScheduler)
                .InvokeCommand(filterPersonsCommand);

            _filteredPersons = filterPersonsCommand.ToProperty(this, vm => vm.Persons, _persons.CreateDerivedCollection(p => p));


        }
        private async Task<IReactiveDerivedList<Person>> FilterPersons(string filter)
        {
            await Task.Delay(500); // Lets say this takes some time
            return _persons.CreateDerivedCollection(p => p, p => p.Name.Contains(filter));
        }
    }
}

The filtering itself works like a charm, also the throttling, when using the GUI.

However, I'd like to unittest the behavior of the filtering and this is my first attempt:

[Test]
public void FilterPersonsByName()
{
    var sut = new MainViewModel();

    sut.Persons.Should().HaveCount(5);
    sut.Filter = "J";
    sut.Persons.Should().HaveCount(2);
}

This test fails because the collection still has 5 people.

When I get rid of the await Task.Delay(500) in FilterPersons then the test will pass, but takes 2 seconds (from the throttle).

  1. Is there a way to have the throttle be instant within the test to speed up the unittest?

  2. How would I test the async behavior in my filter?

I'm using ReactiveUI 7.x

Estis answered 15/4, 2017 at 10:4 Comment(0)
O
2

Short answers:

  1. Yes, by making sure you're using CurrentThreadScheduler.Instance when running under test
  2. Instead of using CurrentThreadScheduler, use a TestScheduler and manually advance it

The longer answer is that you need to ensure your unit tests can control the scheduler being used by your System Under Test (SUT). By default, you'll generally want to use CurrentThreadScheduler.Instance to make things happen "instantly" without any need to advance the scheduler manually. But when you want to write tests that do validate timing, you use a TestScheduler instead.

If, as you seem to be, you're using RxApp.*Scheduler, take a look at the With extension method, which can be used like this:

(new TestScheduler()).With(sched => {
    // write test logic here, and RxApp.*Scheduler will resolve to the chosen TestScheduler
});

I tend to avoid using the RxApp ambient context altogether for the same reason I avoid all ambient contexts: they're shared state and can cause trouble as a consequence. Instead, I inject an IScheduler (or two) into my SUT as a dependency.

Orta answered 15/4, 2017 at 12:19 Comment(3)
Thank you for your reply. What would you inject into the Model when running as production code? The RxApp.MainThreadScheduler?Estis
Update: [Test] public void FilterPersonsByName() { new TestScheduler().With(sched => { var sut = new MainViewModel(sched); sched.AdvanceByMs(3000); sut.Persons.Should().HaveCount(5); sut.Filter = "J"; sched.AdvanceByMs(3000); sut.Persons.Should().HaveCount(2); }); } works. So the 2nd point is workign out :-) If I give it the CurrentThreadScheduler.Instace it still runs 2-3 seconds instead of being instantEstis
You can certainly use RxApp schedulers at runtime if you like. Personally, I create schedulers myself and inject them, but that's personal preference, not necessity.Orta

© 2022 - 2024 — McMap. All rights reserved.