1

Boiled down: I need to delay execution in my unit test thread so that an observable has time to update a property. Is there a Reactive way to do this without having to resort to Thread.Sleep?

I have a ViewModel that manages a ReactiveList of "Thing". Both ThingViewModel and the main ViewModel are derived from ReactiveObject. Thing has an IsSelected property and the ViewModel has a SelectedThing property that I keep synched based on observing changes on IsSelected. I have a couple of different views that use the ViewModel and this allows me to synch the selected Things between those views nicely. And it works. The problem comes when I try to unit test this interaction.

The ViewModel has this subscription in its constructor:

Things.ItemChanged.Where(c => c.PropertyName.Equals(nameof(ThingViewModel.IsSelected))).ObserveOn(ThingScheduler).Subscribe(c =>
{
    SelectedThing = Things.FirstOrDefault(thing => thing.IsSelected);
});

In my unit test, this assert always fails:

thingVM.IsSelected = true;
Assert.AreEqual(vm.SelectedThing, thingVM);

But this assert always passes:

thingVM.IsSelected = true;
Thread.Sleep(5000);
Assert.AreEqual(vm.SelectedThing, thingVM);

Essentially, I need to wait long enough for the subscription to complete the change. (I don't really need that long to wait as when running in the UI, it's pretty snappy.)

I tried adding an observer in my unit test to wait on that processing, hoping they'd be similar. But that's been a wash. Here's what didn't work.

var itemchanged = vm.Things.ItemChanged.Where(x => x.PropertyName.Equals("IsSelected")).AsObservable();
. . . 
thingVM.IsSelected = true;
var changed = itemChanged.Next();
Assert.AreEqual(vm.SelectedThing, thingVM);

Moving the itemChanged.Next around didn't help, nor did triggering iteration by calling .First() or .Any() on changed (both of those hung the process as it blocked the thread for a notification that never occurred).

So. Is there a Reactive way to wait on that interaction so that I can Assert that the property change is happening correctly? I messed around some with TestScheduler (I needed manual Scheduler setting for the UI anyway, so this wasn't hard), but that doesn't seem to apply, as the actual action given to the scheduler happens in my ViewModel on ItemChanged and I couldn't find a way to make that trigger in the ways TestScheduler seems set up to work.

Jacob Proffitt
  • 12,664
  • 3
  • 41
  • 47
  • If the target is INPC derived then plug/subscribe to the property changed event and set a flag that can be asserted. – Nkosi Jan 31 '18 at 07:08
  • ReactiveObject supports INPC, but since I'm playing in Reactive Land, an assert in an event handler would be less than ideal. If I wanted to go that route, I could subscribe to the property changed observable and assert there. I'd prefer to assert on the actual property as above, but I may use that as a fallback... – Jacob Proffitt Jan 31 '18 at 18:04
  • That may suffice in this one instance. But then, so does the Thread.Sleep solution. The reason for the question is that I want to know if there's a way to do this that is "Reactive Native". If there is, then I'll learn something new and useful about Reactive that will apply beyond just this moment. – Jacob Proffitt Jan 31 '18 at 18:12
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/164293/discussion-between-jacob-proffitt-and-nkosi). – Jacob Proffitt Jan 31 '18 at 18:16
  • Can you make the setter of `SelectedThing` protected and override it or mock it in your unit-test so you can intercept the set-action? – huysentruitw Feb 01 '18 at 20:00
  • 1
    I probably *should* make the set private, actually, as I don't want anything but the ViewModel setting the property. Making it protected just so I can create an override/mock is more pain than I'm looking for, though. – Jacob Proffitt Feb 02 '18 at 00:12

2 Answers2

2

You have a few different solutions here. First off for the sake of completion let's create a simple little ViewModel with a backing model.

public class Foo : ReactiveObject
{
    private Bar selectedItem;
    public Bar SelectedItem
    {
        get => selectedItem;
        set => this.RaiseAndSetIfChanged(ref selectedItem, value);
    }

    public ReactiveList<Bar> List { get; }

    public Foo()
    {
        List = new ReactiveList<Bar>();
        List.ChangeTrackingEnabled = true;

        List.ItemChanged
            .ObserveOn(RxApp.TaskpoolScheduler)
            .Subscribe(_ => { SelectedItem = List.FirstOrDefault(x => x.IsSelected); });
    }
}

public class Bar : ReactiveObject
{
    private bool isSelected;
    public bool IsSelected
    {
        get => isSelected;
        set => this.RaiseAndSetIfChanged(ref isSelected, value);
    }
}

A hacky fix (which I wouldn't recommend) is to change ObserveOn to SubscribeOn. See answer here for a better explanation: https://stackoverflow.com/a/28645035/5622895

The recommendation I'd give is to import reactiveui-testing into your unit tests. At that point you can override the schedulers with an ImmediateScheduler, this will force everything to schedule immediately and on a single thread

    [TestMethod]
    public void TestMethod1()
    {
        using (TestUtils.WithScheduler(ImmediateScheduler.Instance))
        {
            var bar = new Bar();
            var foo = new Foo();
            foo.List.Add(bar);
            bar.IsSelected = true;
            Assert.AreEqual(bar, foo.SelectedItem);
        }
    }
David Haxton
  • 283
  • 1
  • 10
  • It'll be Monday before I can give this a real try, but it looks very interesting! – Jacob Proffitt Feb 04 '18 at 07:54
  • It took some finagling (switching my ViewModel to use RxApp.TaskpoolScheduler *and* ensuring that ViewModel instantiation happened within the "using") but once I figured out the parameters, this worked as expected. Well done. – Jacob Proffitt Feb 05 '18 at 16:56
1

Playing around with the simple idea of subscribing to the the PropertyChanged event I created this extension method

public static Task OnPropertyChanged<T>(this T target, string propertyName) where T : INotifyPropertyChanged {
    var tcs = new TaskCompletionSource<object>();
    PropertyChangedEventHandler handler = null;
    handler = (sender, args) => {
        if (string.Equals(args.PropertyName, propertyName, StringComparison.InvariantCultureIgnoreCase)) {
            target.PropertyChanged -= handler;
            tcs.SetResult(0);
        }
    };
    target.PropertyChanged += handler;
    return tcs.Task;
}

that would wait for the property changed event to be raised instead of having to block the thread.

For example,

public async Task Test() {

    //...

    var listener = vm.OnPropertyChanged("SelectedThing");
    thingVM.IsSelected = true;
    await listener;
    Assert.AreEqual(vm.SelectedThing, thingVM);
}

I then refined it further using expressions to get away from the magic strings and also return the value of the property being watched.

public static Task<TResult> OnPropertyChanged<T, TResult>(this T target, Expression<Func<T, TResult>> propertyExpression) where T : INotifyPropertyChanged {
    var tcs = new TaskCompletionSource<TResult>();
    PropertyChangedEventHandler handler = null;

    var member = propertyExpression.GetMemberInfo();
    var propertyName = member.Name;
    if (member.MemberType != MemberTypes.Property)
        throw new ArgumentException(string.Format("{0} is an invalid property expression", propertyName));

    handler = (sender, args) => {
        if (string.Equals(args.PropertyName, propertyName, StringComparison.InvariantCultureIgnoreCase)) {
            target.PropertyChanged -= handler;
            var value = propertyExpression.Compile()(target);
            tcs.SetResult(value);
        }
    };
    target.PropertyChanged += handler;
    return tcs.Task;
}

/// <summary>
/// Converts an expression into a <see cref="System.Reflection.MemberInfo"/>.
/// </summary>
/// <param name="expression">The expression to convert.</param>
/// <returns>The member info.</returns>
public static MemberInfo GetMemberInfo(this Expression expression) {
    var lambda = (LambdaExpression)expression;

    MemberExpression memberExpression;
    if (lambda.Body is UnaryExpression) {
        var unaryExpression = (UnaryExpression)lambda.Body;
        memberExpression = (MemberExpression)unaryExpression.Operand;
    } else
        memberExpression = (MemberExpression)lambda.Body;

    return memberExpression.Member;
}

And used like

public async Task Test() {  

    //...

    var listener = vm.OnPropertyChanged(_ => _.SelectedThing);
    thingVM.IsSelected = true;
    var actual = await listener;
    Assert.AreEqual(actual, thingVM);
}
Nkosi
  • 235,767
  • 35
  • 427
  • 472
  • I'd vastly prefer an Rx-based solution. Using an event to detect the completion of a Rx-based change feels kind of like going backwards. It does have the advantage that I believe it'd actually work, though... – Jacob Proffitt Feb 02 '18 at 22:30
  • @JacobProffitt I just left this as a potential alternative if no other solution presented itself. I would also mention that if this is to be used primarily as a utility in your unit tests then it shouldn't really affect your core code base in terms of how this was designed. I've tested it and it does in fact work. – Nkosi Feb 03 '18 at 16:37
  • It's a thoughtful answer and accomplishes the task. I'm holding out hope for a more Rx-centric one, but it's a small, anemic hope at this point... :) – Jacob Proffitt Feb 03 '18 at 20:19