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

How to unit test RelayCommand that Executes an async method?

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

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

我没有使用 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(...); 调用之后有额外的逻辑,所以这可能不适用于您当前的问题。但是,这可能对您需要为视图模型编写的其他单元测试有所帮助。

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

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

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

1。测试 RelayCommand 触发器

1.a 使用外部依赖来验证

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

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 逻辑

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