gpt4 book ai didi

c# - 绑定(bind)到 ReactiveCommand IsExecuting 的单元测试 ViewModel 属性

转载 作者:行者123 更新时间:2023-11-30 17:30:41 29 4
gpt4 key购买 nike

我是 ReactiveUI 的新手,正在按照 here 中的示例进行操作,以及我进行的单元测试。

正如预期的那样,示例代码完美运行,但我的单元测试断言当我的 ReactiveCommand< 的 IsExecuting 属性时 SpinnerVisibility 属性按预期更改 改变,不改变。

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

public Visibility SpinnerVisibility => _spinnerVisibility.Value;

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

在 View 模型构造函数中,我设置了 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 => 状态? 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 执行时,statetrue 但随后立即重新执行,state 返回 false,大概是因为被 mock 后,photosProvider.FromFlickr 会立即返回(与正常从 API 检索图像不同),这意味着命令不再执行。

然后我看到了 Paul Bett 对 a similar question 的回应,并在我的模拟中添加了一个 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,所以如果这些东西破坏了您的测试,我会认为这是一件好事。

关于c# - 绑定(bind)到 ReactiveCommand IsExecuting 的单元测试 ViewModel 属性,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/49338867/

29 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com