【问题标题】:How to unit test RelayCommand that Executes an async method?如何对执行异步方法的 RelayCommand 进行单元测试?
【发布时间】:2015-03-18 21:24:39
【问题描述】:

由于没有 RelayCommandAsync(至少我不知道),如何测试这种情况。例如:

public RelayCommand LoadJobCommand
{
    get
    {
        return this.loadJobCommand ?? (
            this.loadJobCommand =
            new RelayCommand(
                this.ExecuteLoadJobCommandAsync));
    }
}

private async void ExecuteLoadJobCommandAsync()
{
   await GetData(...);
}

测试:

vm.LoadJobCommand.Execute()

Assert.IsTrue(vm.Jobs.Count > 0)

【问题讨论】:

  • MSDN 杂志posted a article 显示AsyncCommand(基本上是RelayCommandAsync 的实现),它提供了一个public async Task ExecuteAsync(object parameter) 你可以等待。
  • @ScottChamberlain - 该链接与 mvvm-light 和 RelayCommands 有什么关系?
  • 它没有。 mvvm-light 没有您需要的东西,因此您需要编写自己的异步中继命令(如果您希望单元测试能够调用该命令)。那篇文章给你一个实现。因为这不是 mvvm-light 中的解决方案,所以我将其作为评论而不是答案,因为我觉得它并没有“回答你的问题”,而是给了你另一个选择。
  • @ScottChamberlain - 明白,我会尝试先询问 mvvm-light 团队,看看他们是否有异步 RelayCommand 正在工作中,并暂时使用 InternalsVisableTo,除非有人回答更好的解决方案下面。

标签: c# unit-testing asynchronous mvvm-light .net-4.5.2


【解决方案1】:

这实际上取决于您要测试的内容:

  1. 测试RelayCommand 是否正确连接并调用您的async 方法?

  1. 测试Async方法逻辑是否正确?

1。测试 RelayCommand 触发器

1.a 使用外部依赖进行验证

根据我的个人经验,最简单的方法是测试触发器是否正确连接以执行命令,然后测试您的类是否已按预期与另一个外部类交互。例如

private async void ExecuteLoadJobCommandAsync()
{
   await GetData(...);
}

private async void GetData(...)
{
   var data = await _repo.GetData();
   Jobs.Add(data);
}

测试你的 repo 是否被调用相当容易。

    public void TestUsingExternalDependency()
    {
        _repo.Setup(r => r.GetData())
            .Returns(Task.Run(() => 5))
            .Verifiable();

        _vm.LoadJobCommand.Execute(null);

        _repo.VerifyAll();
    }

有时我什至会这样做,以免它尝试处理所有内容:

    [Test]
    public void TestUsingExternalDependency()
    {
        _repo.Setup(r => r.GetData())
            .Returns(() => { throw new Exception("TEST"); })
            .Verifiable();

        try
        {
            _vm.LoadJobCommand.Execute(null);
        }
        catch (Exception e)
        {
            e.Message.Should().Be("TEST");
        }

        _repo.VerifyAll();
    }

1.b 使用调度器

另一种选择是使用调度器,并使用它来调度任务。

public interface IScheduler
{
    void Execute(Action action);
}

// Injected when not under test
public class ThreadPoolScheduler : IScheduler
{
    public void Execute(Action action)
    {
        Task.Run(action);
    }
}

// Used for testing
public class ImmediateScheduler : IScheduler
{
    public void Execute(Action action)
    {
        action();
    }
}

然后在你的 ViewModel 中

    public ViewModelUnderTest(IRepository repo, IScheduler scheduler)
    {
        _repo = repo;
        _scheduler = scheduler;
        LoadJobCommand = new RelayCommand(ExecuteLoadJobCommandAsync);
    }
    private void ExecuteLoadJobCommandAsync()
    {
        _scheduler.Execute(GetData);

    }

    private void GetData()
    {
        var a =  _repo.GetData().Result;
        Jobs.Add(a);
    }

你的测试

    [Test]
    public void TestUsingScheduler()
    {
        _repo.Setup(r => r.GetData()).Returns(Task.Run(() => 2));

        _vm = new ViewModelUnderTest(_repo.Object, new ImmediateScheduler());

        _vm.LoadJobCommand.Execute(null);

        _vm.Jobs.Should().NotBeEmpty();
    }

2。测试GetData 逻辑

如果您要测试获取 GetData() 逻辑,甚至是 ExecuteLoadJobCommandAsync() 逻辑。那么你绝对应该将你想要测试的方法设置为 Internal,并将你的程序集标记为 InternalsVisibleTo,这样你就可以直接从你的测试类中调用这些方法。

【讨论】:

  • “这真的取决于你要测试的内容”是我试图在我的回答中表达的观点,但你以更好的方式做到了:)
  • @Michal - 这很好,我没有提到它,但我正在编写一个 Windows 商店应用程序,除非发生了变化,否则由于这些变化,模拟框架在 Windows 商店应用程序中不起作用在一些反射的东西。所以看起来仍然像 InternalsVisibleTo
  • @O.O 会尝试一下,如果没有任何适用于 Windows 商店应用程序目标的模拟框架,我会感到非常惊讶。
  • @O.O experimental branch of Moq 适用于 WinRT(Windows 商店)应用程序。您可能还对此 SO 问题中给出的其他答案感兴趣:What is the mocking framework of choice in Unit Test Library for Windows Store Applications?
  • @MichalCiechan - 去调度程序路线。
【解决方案2】:

为什么不用测试覆盖 GetData(...) 方法?我认为测试中继命令没有任何意义

【讨论】:

  • 不是在测试 RelayCommands,而是在测试 RelayCommand 调用的私有方法。
  • 你在强调这一点。视图模型由中继命令触发,因此您需要测试该触发器是否得到正确处理。
  • @O.O 实现它的一种方法是创建方法internal,然后使用InternalsVisableTo 允许单元测试访问该方法
  • @PhilipStuyck 但它不是 ui 测试的业务吗?我认为单元测试仅适用于模型
  • @ScottChamberlain - 有道理,我可能会试一试。
【解决方案3】:

我没有使用 async/await,但我过去曾遇到过类似的问题。我所处的情况是在其自身内部调用 Task.Run( 的方法,并且单元测试正在验证 ViewModel 以正确的次数和正确的参数调用服务。

我们解决这个问题的方法是,我们使用 ManualResetEventSlim 对被调用的服务进行 Mock,然后单元测试在继续之前等待调用该重置事件。

[TestMethod]
public void EXAMPLE()
{
    using (var container = new UnityAutoMoqContainer())
    {
        //(SNIP)

        var serviceMock = container.GetMock<ITreatmentPlanService>();

        var resetEvent = new ManualResetEventSlim();

        serviceMock.Setup(x=>x.GetSinglePatientViewTable(dateWindow, currentPatient, false))
            .Returns(() =>
            {
                resetEvent.Set();
                return new ObservableCollection<SinglePatientViewDataRow>();
            });

        var viewModel = container.Resolve<SinglePatientViewModel>();

        //(SNIP)

        viewModel.PatientsHadTPClosed(guids, Guid.NewGuid());

        waited = resetEvent.Wait(timeout);
        if(!waited)
            Assert.Fail("GetSinglePatientViewTable was not called within the timeout of {0} ms", timeout);

        //(SNIP)

        serviceMock.Verify(x => x.GetSinglePatientViewTable(dateWindow, currentPatient, false), Times.Once);
    }
}

这种方法是否适用于您完全取决于您的单元测试实际测试的是什么。因为您检查了Assert.IsTrue(vm.Jobs.Count &gt; 0),所以看起来您在await GetData(...); 调用之后正在执行额外的逻辑,因此这可能不适用于您当前的问题。但是,这可能对您需要为视图模型编写的其他单元测试有所帮助。

【讨论】:

  • 模拟不是一个选项,不幸的是,(Windows 商店应用程序),我仍在调查你之前评论过的异步中继命令。
猜你喜欢
  • 1970-01-01
  • 2013-03-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-11-13
  • 2014-12-30
相关资源
最近更新 更多