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

Unit Testing ItemChanged on ReactiveList Triggering Property Change

归结为:我需要在我的单元测试线程中延迟执行,以便 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);

本质上,我需要等待足够长的时间让订阅完成更改。 (我真的不需要等那么久,因为 UI 中的 运行 非常快。)

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

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 似乎设置工作的方式触发该触发器的方法。

尝试订阅 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);
}

然后我使用表达式进一步改进它以摆脱魔法字符串以及 return 正在监视的 属性 的值。

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

这里有几个不同的解决方案。首先,为了完成让我们创建一个带有支持模型的简单的小 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。请在此处查看答案以获得更好的解释:

我给出的建议是将 reactiveui-testing 导入到您的单元测试中。那时你可以用 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);
        }
    }