【问题标题】:Unit Testing ItemChanged on ReactiveList Triggering Property ChangeReactiveList 触发属性更改时的单元测试 ItemChanged
【发布时间】:2018-02-04 13:26:53
【问题描述】:

归结:我需要在我的单元测试线程中延迟执行,以便可观察对象有时间更新属性。有没有一种响应式方法可以做到这一点而不必求助于 Thread.Sleep?

我有一个管理“事物”的 ReactiveList 的 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 没有帮助,也没有通过在 changed 上调用 .First() 或 .Any() 来触发迭代(这两个都挂起进程,因为它阻塞了线程以获得永远不会的通知发生)。

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

【问题讨论】:

  • 如果目标是 INPC 派生的,则插入/订阅属性更改事件并设置可以断言的标志。
  • ReactiveObject 支持 INPC,但由于我在 Reactive Land 中玩游戏,因此事件处理程序中的断言不太理想。如果我想走那条路,我可以订阅改变 observable 的属性并在那里断言。我更愿意像上面那样断言实际属性,但我可以将其用作后备...
  • 这可能就足够了。但是,Thread.Sleep 解决方案也是如此。问题的原因是我想知道是否有一种方法可以做到这一点,即“反应式原生”。如果有的话,那么我会学到一些关于 Reactive 的新东西和有用的东西,这些东西将适用于这一刻。
  • 我可能应该将集合设为私有,实际上,因为除了 ViewModel 设置属性之外我什么都不想要。不过,让它受到保护以便我可以创建覆盖/模拟比我想要的更痛苦。

标签: c# unit-testing reactiveui


【解决方案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);
        }
    }

【讨论】:

  • 星期一我才能真正尝试一下,但它看起来很有趣!
  • 花了一些功夫(将我的 ViewModel 切换为使用 RxApp.TaskpoolScheduler 确保 ViewModel 实例化发生在“使用”中)但是一旦我弄清楚了参数,这就奏效了正如预期的那样。干得好。
【解决方案2】:

玩弄订阅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);
}

【讨论】:

  • 我非常喜欢基于 Rx 的解决方案。使用事件来检测基于 Rx 的更改的完成感觉有点像倒退。它确实具有我相信它确实可以工作的优势......
  • @JacobProoffitt 如果没有其他解决方案出现,我只是将其作为潜在的替代方案。我还要提一下,如果这主要用作单元测试中的实用程序,那么就其设计方式而言,它不应该真正影响您的核心代码库。我已经对其进行了测试,它确实有效。
  • 这是一个深思熟虑的答案并完成了任务。我对一个更以 Rx 为中心的希望抱有希望,但在这一点上,希望渺茫……:)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-01-08
  • 2018-02-15
  • 2012-02-01
  • 2011-06-01
  • 1970-01-01
  • 2021-03-14
相关资源
最近更新 更多