单元测试绑定到ReactiveCommand IsExecuting的ViewModel属性

时间:2018-03-17 15:56:41

标签: c# unit-testing system.reactive

我是ReactiveUI的新手,我正在按照SKNode Apple's documentation列出的示例,并在我去的时候进行单元测试。

正如预期的那样,示例代码工作正常,但我的单元测试断言SpinnerVisibility属性在我IsExecuting的{​​{1}}属性发生更改时按预期发生更改。 / p>

根据示例,我在视图模型上有一个属性,用于微调器可见性和执行搜索的命令:

ReactiveCommand

在视图模型构造函数中,我设置了public Visibility SpinnerVisibility => _spinnerVisibility.Value; public ReactiveCommand<string, List<FlickrPhoto>> ExecuteSearch { get; protected set; } 命令,ExecuteSearch设置为在命令执行时更改:

SpinnerVisibility

我最初的尝试是直接调用命令:

public AppViewModel(IGetPhotos photosProvider)
{
    ExecuteSearch = ReactiveCommand.CreateFromTask<string, List<FlickrPhoto>>(photosProvider.FromFlickr);

    this.WhenAnyValue(search => search.SearchTerm)
        .Throttle(TimeSpan.FromMilliseconds(800), RxApp.MainThreadScheduler)
        .Select(searchTerm => searchTerm?.Trim())
        .DistinctUntilChanged()
        .Where(searchTerm => !string.IsNullOrWhiteSpace(searchTerm))
        .InvokeCommand(ExecuteSearch);

    _spinnerVisibility = ExecuteSearch.IsExecuting
        .Select(state => state ? Visibility.Visible : Visibility.Collapsed)
        .ToProperty(this, model => model.SpinnerVisibility, Visibility.Hidden);
}

这确实导致[Test] public void SpinnerVisibility_ShouldChangeWhenCommandIsExecuting() { var photosProvider = A.Fake<IGetPhotos>(); var fixture = new AppViewModel(photosProvider); fixture.ExecuteSearch.Execute().Subscribe(_ => { fixture.SpinnerVisibility.Should().Be(Visibility.Visible); }); fixture.SpinnerVisibility.Should().Be(Visibility.Collapsed); } lambda被执行,但后续的断言因某些原因失败state => state ? Visibility.Visible : Visibility.Collapsed仍为SpinnerVisibility

我的下一次尝试是通过使用Collapsed

模拟搜索来间接调用命令
TestScheduler

和以前一样,lambda执行,[Test] public void SpinnerVisibility_ShouldChangeWhenCommandIsExecuting() { new TestScheduler().With(scheduler => { var photosProvider = A.Fake<IGetPhotos>(); var fixture = new AppViewModel(photosProvider); A.CallTo(() => photosProvider.FromFlickr(A<string>.Ignored)).ReturnsLazily( () => new List<FlickrPhoto> { new FlickrPhoto { Description = "a thing", Title = "Thing", Url = "https://thing.com" } }); fixture.SearchTerm = "foo"; scheduler.AdvanceByMs(801); // search is throttled by 800ms fixture.SpinnerVisibility.Should().Be(Visibility.Visible); }); } state,但随后立即重新执行,true回到state,大概是因为被嘲笑false 1}}会立即返回(与通常从API检索图像不同),这意味着该命令不再执行。

然后我遇到了Paul Bett对here的回复,并在我的模拟中添加了photosProvider.FromFlickr

Observable.Interval

和相应的测试更改:

A.CallTo(() => photosProvider.FromFlickr(A<string>.Ignored)).ReturnsLazily(
                    () =>
                    {
                        Observable.Interval(TimeSpan.FromMilliseconds(500), scheduler);
                        return new List<FlickrPhoto> {new FlickrPhoto {Description = "a thing", Title = "Thing", Url = "https://thing.com"}};
                    });

这没有效果。

最后,我等待scheduler.AdvanceByMs(501); fixture.SpinnerVisibility.Should().Be(Visibility.Collapsed);

Interval

这允许A.CallTo(() => photosProvider.FromFlickr(A<string>.Ignored)).ReturnsLazily(async () => { await Observable.Interval(TimeSpan.FromMilliseconds(500), scheduler); return new List<FlickrPhoto> {new FlickrPhoto {Description = "a thing", Title = "Thing", Url = "https://thing.com"}}; }); 断言通过,但现在无论我推进调度程序多远,模拟方法似乎永远不会返回,因此后续断言失败。

这种方法是否使用fixture.SpinnerVisibility.Should().Be(Visibility.Visible)正确/建议?如果是这样,我错过了什么?如果没有,应该如何测试这种行为?

1 个答案:

答案 0 :(得分:1)

首先,你试图在一次测试中测试两个独立的东西。将逻辑分离到更集中的测试将使您在重构时更少的头痛。请考虑以下内容:

  1. SearchTerm_InvokesExecuteSearchAfterThrottle
  2. SpinnerVisibility_VisibleWhenExecuteSearchIsExecuting
  3. 现在您有单元测试,它们分别验证每项功能。如果一个失败,你就会确切地知道哪个期望被打破,因为只有一个。现在,进行实际测试......

    根据您的代码,我假设您使用的是NUnitFakeItEasyMicrosoft.Reactive.Testing。测试可观察量的推荐策略是使用TestScheduler并断言可观察量的最终结果。

    以下是我将如何实施它们:

    using FakeItEasy;
    using Microsoft.Reactive.Testing;
    using NUnit.Framework;
    using ReactiveUI;
    using ReactiveUI.Testing;
    using System;
    using System.Reactive.Concurrency;
    
    ...
    
    public sealed class AppViewModelTest : ReactiveTest
    {
        [Test]
        public void SearchTerm_InvokesExecuteSearchAfterThrottle()
        {
            new TestScheduler().With(scheduler =>
            {
                var sut = new AppViewModel(A.Dummy<IGetPhotos>());
    
                scheduler.Schedule(() => sut.SearchTerm = "A");
                scheduler.Schedule(TimeSpan.FromTicks(200), () => sut.SearchTerm += "B");
                scheduler.Schedule(TimeSpan.FromTicks(300), () => sut.SearchTerm += "C");
                scheduler.Schedule(TimeSpan.FromTicks(400), () => sut.SearchTerm += "D");
                var results = scheduler.Start(
                    () => sut.ExecuteSearch.IsExecuting,
                    0, 100, TimeSpan.FromMilliseconds(800).Ticks + 402);
    
                results.Messages.AssertEqual(
                    OnNext(100, false),
                    OnNext(TimeSpan.FromMilliseconds(800).Ticks + 401, true)
                );
            });
        }
    
        [Test]
        public void SpinnerVisibility_VisibleWhenExecuteSearchIsExecuting()
        {
            new TestScheduler().With(scheduler =>
            {
                var sut = new AppViewModel(A.Dummy<IGetPhotos>());
    
                scheduler.Schedule(TimeSpan.FromTicks(300),
                    () => sut.ExecuteSearch.Execute().Subscribe());
                var results = scheduler.Start(
                    () => sut.WhenAnyValue(x => x.SpinnerVisibility));
    
                results.Messages.AssertEqual(
                    OnNext(200, Visibility.Collapsed),
                    OnNext(301, Visibility.Visible),
                    OnNext(303, Visibility.Collapsed));
            });
        }
    }
    

    请注意,甚至不需要伪造/模拟IGetPhotos,因为您的测试不会根据命令的持续时间进行任何验证。他们只关心何时执行

    有些事情一开始可能难以包裹,例如当实际发生嘀嗒时,但是一旦你掌握它就会非常强大。关于在测试中使用ReactiveUI的问题可能存在争议(例如IsExecutingWhenAnyValue),但我认为这样可以保持简洁。另外,无论如何你都在你的应用程序中使用ReactiveUI,所以如果这些事情打破了我的考验,我认为这是件好事。