[英]Unit testing ViewModel property bound to ReactiveCommand IsExecuting
我是ReactiveUI的新手,我正在按照这里列出的示例进行操作,并按照我的要求进行单元测试。
正如预期的那样,示例代码完美地工作,但是当我的ReactiveCommand
的IsExecuting
属性发生更改时,我的单元测试断言SpinnerVisibility
属性按预期更改。
根据示例,我在视图模型上具有用于微调器可见性的属性和用于执行搜索的命令:
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);
}
这确实导致state => state ? Visibility.Visible : Visibility.Collapsed
state => state ? Visibility.Visible : Visibility.Collapsed
lambda正在执行,但随后的断言失败,因为某些原因SpinnerVisibility
仍然是Collapsed
。
我的下一次尝试是通过使用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);
});
}
和以前一样,lambda执行, state
为true
,然后立即重新执行, state
返回false
,大概是因为,被photosProvider.FromFlickr
, photosProvider.FromFlickr
会立即返回(不像通常从API检索图像),这意味着命令不再执行。
然后我遇到了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"}};
});
和相应的测试变化:
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)
断言传递,但现在无论我推进调度程序多远,模拟的方法似乎永远不会返回,因此后续的断言失败。
这种方法使用TestScheduler
正确/建议的吗? 如果是这样,我错过了什么? 如果没有,应该如何测试这种行为?
首先,你试图在一次测试中测试两个独立的东西。 将逻辑分离到更集中的测试将使您在重构时更少的头痛。 请考虑以下内容:
SearchTerm_InvokesExecuteSearchAfterThrottle
SpinnerVisibility_VisibleWhenExecuteSearchIsExecuting
现在,您有单独的单元测试验证每个功能。 如果一个失败,你就会确切地知道哪个期望被打破,因为只有一个。 现在,进行实际测试......
根据您的代码,我假设您使用的是NUnit
, FakeItEasy
和Microsoft.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(例如IsExecuting
, WhenAnyValue
)可能会有一些争论,但我认为它使事情简洁。 另外,无论如何你都在你的应用程序中使用ReactiveUI,所以如果这些事情打破了你的考验,我认为这是一件好事。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.