如何对执行异步方法的RelayCommand进行单元测试?

7

由于没有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)

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

1
我以前没有使用async/await,但我遇到过类似的问题。我所处的情况是该方法在内部调用了Task.Run(),而单元测试正在验证ViewModel是否以正确的次数和参数调用了服务。
我们解决这个问题的方法是使用一个被调用服务的Mock,它使用了ManualResetEventSlim,然后单元测试在继续之前等待该复位事件被调用。
[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 > 0),看起来在 await GetData(...); 调用之后有额外的逻辑处理,所以这可能不适用于你当前的问题。然而,这可能对你需要编写的其他视图模型单元测试有所帮助。

遗憾的是,模拟不是一个选项(Windows Store 应用程序),我仍在研究你之前评论的异步 RelayCommand。 - O.O

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);
}

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

    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()逻辑。那么你应该将要测试的方法设置为内部方法,并将你的程序集标记为InternalsVisibleTo,以便您可以直接从测试类中调用这些方法。


1
“这取决于你想要测试什么”是我在回答中想要表达的观点,但你用更好的方式表达了它 :) - Scott Chamberlain
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - O.O
@O.O会试一下,如果没有任何可以在Windows Store应用程序目标上工作的模拟框架,我会非常惊讶。 - Michal Ciechan
@MichalCiechan - 我打算走调度程序的路线。 - O.O
为什么这被标记为正确答案?在你的例子中,VerifyAll不会等待命令执行。你可以通过在命令中设置Task.Delay来检查它。 - WhoKnows
显示剩余2条评论

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

2
这不是在测试RelayCommands,而是在测试RelayCommand调用的私有方法。 - O.O
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Philip Stuyck
1
一种实现方法是将该方法设为“internal”,然后使用InternalsVisableTo允许单元测试访问该方法。 - Scott Chamberlain
@PhilipStuyck 但这不是UI测试的事吗?我认为单元测试只针对模型。 - rum
@ScottChamberlain - 那很有道理,我可能会试一下。 - O.O
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接