单元测试 视图模型 属性绑定到 ReactiveCommand 正在执行



我是ReactiveUI的新手,正在遵循此处列出的示例,并随时进行单元测试。

正如预期的那样,示例代码运行良好,但我的单元测试断言,当我ReactiveCommandIsExecuting属性更改时,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);
}

这确实导致 lambdastate => state ? Visibility.Visible : Visibility.Collapsed被执行,但由于某种原因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 执行,statetrue但随后立即重新执行,state回到false,大概是因为被嘲笑,photosProvider.FromFlickr会立即返回(与正常从 API 检索图像不同),这意味着命令不再执行。

然后我看到了保罗·贝特对类似问题的回答,并在我的模拟中添加了一个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正确/建议吗?如果是这样,我错过了什么?如果不是,应该如何测试这种类型的行为?

首先,您尝试在一个测试中测试两个独立的东西。将逻辑分离到更集中的测试中,将来重构时会减少头痛。请考虑以下事项:

  1. SearchTerm_InvokesExecuteSearchAfterThrottle
  2. SpinnerVisibility_VisibleWhenExecuteSearchIsExecuting

现在,您有了单元测试,这些单元测试将单独验证每个功能。如果一个失败了,你会确切地知道哪个期望被打破了,因为只有一个。现在,进入实际测试...

根据您的代码,我假设您使用的是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,因此,如果这些东西破坏了您的测试,我认为这是一件好事。

最新更新