简体   繁体   English

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

[英]Unit testing ViewModel property bound to ReactiveCommand IsExecuting

I'm new to ReactiveUI and am following the example set out here , and unit testing as I go. 我是ReactiveUI的新手,我正在按照这里列出的示例进行操作,并按照我的要求进行单元测试。

As expected, the sample code works perfectly, but my unit test which asserts that the SpinnerVisibility property changes as expected when the IsExecuting property of my ReactiveCommand changes, does not. 正如预期的那样,示例代码完美地工作,但是当我的ReactiveCommandIsExecuting属性发生更改时,我的单元测试断言SpinnerVisibility属性按预期更改。

As per the sample, I have properties on my view model for a spinner visibility and a command to execute a search: 根据示例,我在视图模型上具有用于微调器可见性的属性和用于执行搜索的命令:

public Visibility SpinnerVisibility => _spinnerVisibility.Value;

public ReactiveCommand<string, List<FlickrPhoto>> ExecuteSearch { get; protected set; }

And in the view model constructor I set up the ExecuteSearch command and SpinnerVisibility is set to change when the command is executing: 在视图模型构造函数中,我设置了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);
}

My initial attempt was to directly invoke the command: 我最初的尝试是直接调用命令:

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

This did result in the state => state ? Visibility.Visible : Visibility.Collapsed 这确实导致state => state ? Visibility.Visible : Visibility.Collapsed state => state ? Visibility.Visible : Visibility.Collapsed lambda being executed, but the subsequent assertion fails as for some reason SpinnerVisibility is still Collapsed . state => state ? Visibility.Visible : Visibility.Collapsed lambda正在执行,但随后的断言失败,因为某些原因SpinnerVisibility仍然是Collapsed

My next attempt was to indirectly invoke the command by emulating a search using TestScheduler : 我的下一次尝试是通过使用TestScheduler模拟搜索来间接调用该命令:

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

As before, the lambda executes, state is true but then instantly re-executes, with state back to false , presumably because, being mocked, photosProvider.FromFlickr would return instantly (unlike retrieving images from the API normally), which would then mean the command was no longer executing. 和以前一样,lambda执行, statetrue ,然后立即重新执行, state返回false ,大概是因为,被photosProvider.FromFlickrphotosProvider.FromFlickr会立即返回(不像通常从API检索图像),这意味着命令不再执行。

I then came across Paul Bett's response to a similar question , and added an Observable.Interval to my mock: 然后我遇到了Paul Bett对类似问题的回答 ,并在我的模拟中添加了一个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"}};
                    });

and the corresponding test changes: 和相应的测试变化:

scheduler.AdvanceByMs(501);
fixture.SpinnerVisibility.Should().Be(Visibility.Collapsed);

This had no effect. 这没有效果。

Finally, I awaited the Interval : 最后,我等待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"}};
                    });

This allowed the fixture.SpinnerVisibility.Should().Be(Visibility.Visible) assertion to pass, but now regardless how far I advance the scheduler, the mocked method never seems to return and so the subsequent assertion fails. 这允许fixture.SpinnerVisibility.Should().Be(Visibility.Visible)断言传递,但现在无论我推进调度程序多远,模拟的方法似乎永远不会返回,因此后续的断言失败。

Is this approach using TestScheduler correct/advised? 这种方法使用TestScheduler正确/建议的吗? If so, what am I missing? 如果是这样,我错过了什么? If not, how should this type of behaviour be tested? 如果没有,应该如何测试这种行为?

First off, you are trying to test two independent things in one test. 首先,你试图在一次测试中测试两个独立的东西。 Separating the logic into more focused tests will cause you fewer headaches in the future when refactoring. 将逻辑分离到更集中的测试将使您在重构时更少的头痛。 Consider the following instead: 请考虑以下内容:

  1. SearchTerm_InvokesExecuteSearchAfterThrottle
  2. SpinnerVisibility_VisibleWhenExecuteSearchIsExecuting

Now you have unit tests that are verifying each piece of functionality individually. 现在,您有单独的单元测试验证每个功能。 If one fails, you'll know exactly which expectation is broken because there is only one. 如果一个失败,你就会确切地知道哪个期望被打破,因为只有一个。 Now, onto the actual tests... 现在,进行实际测试......

Based on your code, I assume you're using NUnit , FakeItEasy , and Microsoft.Reactive.Testing . 根据您的代码,我假设您使用的是NUnitFakeItEasyMicrosoft.Reactive.Testing The recommended strategy for testing observables is to use the TestScheduler and assert the final outcome of the observable. 测试可观察量的推荐策略是使用TestScheduler并断言可观察量的最终结果。

Here is how I would implement them: 以下是我将如何实现它们:

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

Notice there is no need to even fake/mock IGetPhotos because your tests aren't verifying anything based on the duration of the command. 请注意,甚至不需要伪造/模拟IGetPhotos因为您的测试不会根据命令的持续时间验证任何内容。 They just care about when it executes. 他们只关心它何时执行。

Some things can be difficult to wrap your head around at first, such as when a tick actually occurs, but it's very powerful once you get the hang of it. 有些事情一开始可能难以包裹,例如当实际发生嘀嗒时,但是一旦你掌握它就会非常强大。 Some debate could be had about the usage of ReactiveUI in the tests (eg IsExecuting , WhenAnyValue ), but I think it keeps things succinct. 关于在测试中使用ReactiveUI(例如IsExecutingWhenAnyValue )可能会有一些争论,但我认为它使事情简洁。 Plus, you're using ReactiveUI in your application anyway so if those things broke your test I'd consider that a good thing. 另外,无论如何你都在你的应用程序中使用ReactiveUI,所以如果这些事情打破了你的考验,我认为这是一件好事。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM