Reputation: 12768
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.
Upvotes: 1
Views: 617
Reputation: 284
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);
}
}
Upvotes: 2
Reputation: 247133
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);
}
Upvotes: 1