C# 触发属性更改的ReactiveList上的单元测试项已更改

C# 触发属性更改的ReactiveList上的单元测试项已更改,c#,unit-testing,reactiveui,C#,Unit Testing,Reactiveui,归结起来:我需要延迟单元测试线程中的执行,以便可观察对象有时间更新属性。有没有一种无需求助于线程的反应式方法?睡眠 我有一个ViewModel,它管理一个“事物”的反应列表。ThingViewModel和主ViewModel都是从ReactiveObject派生的。Thing有一个IsSelected属性,ViewModel有一个SelectedThing属性,我根据观察IsSelected上的更改来保持同步。我有几个使用ViewModel的不同视图,这允许我在这些视图之间很好地同步所选内容。它

归结起来:我需要延迟单元测试线程中的执行,以便可观察对象有时间更新属性。有没有一种无需求助于线程的反应式方法?睡眠

我有一个ViewModel,它管理一个“事物”的反应列表。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没有帮助,通过调用
上的.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 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;
}

这里有几种不同的解决方案。首先,为了完整起见,让我们创建一个带有支持模型的简单的小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);
    }
}

如果目标是INPC派生的,则插入/订阅属性已更改的事件,并设置一个可断言的标志。ReactiveObject支持INPC,但由于我在Reactive Land中玩,因此事件处理程序中的断言将不太理想。如果我想走这条路,我可以订阅属性changed observable并在那里断言。我更愿意像上面那样对实际属性进行断言,但我可以使用它作为回退…这在这一实例中可能就足够了。但是,线程也是如此。睡眠解决方案。这个问题的原因是,我想知道是否有一种方法可以做到这一点,即“反应型本地”。如果有,那么我将学习一些新的和有用的关于反应式的知识,这些知识将应用于当前之外的领域。让我们来看看。实际上,我可能应该将集合设置为私有,因为我只需要设置属性的ViewModel。不过,为了创建覆盖/模拟而对其进行保护比我所期望的更痛苦。我更喜欢基于Rx的解决方案。使用事件来检测基于Rx的更改的完成感觉有点像倒退。它确实有一个优点,我相信它确实会起作用,不过…@JacobProffitt我只是把它作为一个潜在的替代方案,如果没有其他解决方案出现的话。我还要提到的是,如果这主要是作为单元测试中的一个实用工具使用的,那么它不应该真正影响核心代码库的设计方式。我已经对它进行了测试,它确实起了作用。这是一个深思熟虑的答案,并且完成了任务。我希望能有一个更以Rx为中心的治疗方案,但在这一点上,这只是一个小小的、贫乏的希望……:)星期一我才能真正尝试一下,但它看起来很有趣!这需要一些欺骗(将我的ViewModel切换为使用RxApp.TaskpoolScheduler,并确保ViewModel实例化发生在“使用”中),但一旦我确定了参数,这就如预期的那样起作用了。做得好。
public async Task Test() {  

    //...

    var listener = vm.OnPropertyChanged(_ => _.SelectedThing);
    thingVM.IsSelected = true;
    var actual = await listener;
    Assert.AreEqual(actual, thingVM);
}
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);
    }
}
    [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);
        }
    }