我是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)
正确/建议?如果是这样,我错过了什么?如果没有,应该如何测试这种行为?
答案 0 :(得分:1)
首先,你试图在一次测试中测试两个独立的东西。将逻辑分离到更集中的测试将使您在重构时更少的头痛。请考虑以下内容:
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,所以如果这些事情打破了我的考验,我认为这是件好事。