单元测试ItemChanged on ReactiveList触发属性更改

时间:2018-01-30 17:56:03

标签: c# unit-testing reactiveui

简化:我需要在单元测试线程中延迟执行,以便observable有时间更新属性。是否有一种反应方式来做到这一点,而不必诉诸Thread.Sleep?

我有一个ViewModel来管理" Thing"的ReactiveList。 ThingViewModel和主ViewModel都是从ReactiveObject派生的。 Thing有一个IsSelected属性,ViewModel有一个SelectedThing属性,我根据观察IsSelected的变化保持同步。我有几个使用ViewModel的不同视图,这使我可以很好地同步这些视图之间的选定事物。它有效。当我尝试对这种交互进行单元测试时,问题出现了。

ViewModel在其构造函数中具有此订阅:

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

在我的单元测试中,此断言总是失败:

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

但是这个断言总是通过:

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

基本上,我需要等待足够长的订阅才能完成更改。 (我不需要那么长时间等待在用户界面中运行时,它非常活泼。)

我尝试在单元测试中添加一个观察者来等待处理,希望它们相似。但那是一次洗漱。这是什么无效的。

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

移动itemChanged.Next周围没有帮助,也没有通过调用changed上的.First()或.Any()来触发迭代(两个都挂起了进程,因为它阻塞了线程从未发生的通知。)

因此。是否有一种反应方式等待该交互,以便我可以断言属性更改是否正确发生?我使用TestScheduler搞砸了一些(无论如何我需要为UI设置手动调度程序设置,所以这并不难),但这似乎并不适用,因为调度程序的实际操作发生在我的ViewModel中在ItemChanged上,我无法通过TestScheduler设置的方式找到触发方式。

2 个答案:

答案 0 :(得分:1)

使用简单的想法订阅我创建此扩展方法的PropertyChanged事件

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;
}
等待引发属性更改事件的

,而不是必须阻塞该线程。

例如,

public async Task Test() {

    //...

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

然后我使用表达式进一步优化它以远离魔术字符串并返回正在监视的属性的值。

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;
}

一样使用
public async Task Test() {  

    //...

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

答案 1 :(得分:1)

这里有几个不同的解决方案。首先,为了完成,我们创建一个带有支持模型的简单ViewModel。

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);
    }
}

一个hacky修复(我不建议)是将ObserveOn更改为SubscribeOn。请参阅此处的答案以获得更好的解释:https://stackoverflow.com/a/28645035/5622895

我给出的建议是将电磁测试导入您的单元测试中。此时,您可以使用ImmediateScheduler覆盖调度程序,这将强制所有内容立即安排在单个线程上

    [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);
        }
    }