用于测试调用异步方法的 ICommand 的模式

Patterns for testing ICommand that call async methods

我只是在查看单元测试 (NUnit) 的最佳实践 ICommand and specifically the MvxCommand implementation within MVVMCross

查看模型

public ICommand GetAuthorisationCommand
{
    get { return new MvxCommand(
            async () => await GetAuthorisationToken(),
            () => !string.IsNullOrWhiteSpace(UserName) && !string.IsNullOrWhiteSpace(Password)); }
}

private async Task GetAuthorisationToken()
{
    // ...Do something async
}

单元测试

[Test]
public async Task DoLogonCommandTest()
{
    //Arrange
    ViewModel vm = new ViewModel(clubCache, authorisationCache, authorisationService);

    //Act
    await Task.Run(() => vm.GetAuthorisationToken.Execute(null));

    //Assert
    Assert.Greater(MockDispatcher.Requests.Count, 0);
}

现在我遇到的问题是测试在没有等待异步操作的情况下就通过了,这在从 ICommand 调用异步方法时感觉有点笨拙。

在单元测试这类 ICommand 和异步方法时是否有任何最佳实践?

您可以使用 MvxAsyncCommand(参见:implementation)代替 MvxCommand,并将 GetAuthorisationCommand 的发布类型从 ICommand 更改为 IMvxAsyncCommand(但是该接口 还不能通过 nuget 使用)然后你可以调用

await vm.GetAuthorisationToken.ExecuteAsync();

由于命令是一劳永逸的事件,您不会直接返回完成。 我建议将测试分成两个动作(或者甚至创建两个单元测试)。

  1. 测试命令是否可以执行
  2. 测试异步任务是否return预期结果

大致如下:

//Act
var canExecute = vm.GetAuthorisationToken.CanExecute();
var result = await vm.GetAuthorisationToken();

但是,需要 GetAuthorisationToken 将其保护级别从私有更改为公开以进行单元测试。

或者

您可以使用 AsyncEx 等库,它可以让您等待异步调用的完成。

[Test]
public async Task DoLogonCommandTest()
{
    AsyncContext.Run(() =>
    {
        //Arrange
        ViewModel vm = new ViewModel(clubCache, authorisationCache, authorisationService);

        //Act
        await Task.Run(() => vm.GetAuthorisationToken.Execute(null));
    });

    //Assert
    Assert.Greater(MockDispatcher.Requests.Count, 0);
}

我认为是最好的长期解决方案。

但是,如果您想要在不依赖预发行软件的情况下立即运行的功能,您可以遵循此模式 which I have found helpful when dealing with asynchronous MVVM commands

首先定义一个IAsyncCommand:

interface IAsyncCommand: ICommand
{
  Task ExecuteAsync(object parameter);
}

然后你可以这样定义一个AsyncCommand实现:

public class AsyncCommand: MvxCommand, IAsyncCommand
{
  private readonly Func<Task> _execute;

  public AsyncCommand(Func<Task> execute)
      : this(execute, null)
  {
  }

  public AsyncCommand(Func<Task> execute, Func<bool> canExecute)
      : base(async () => await execute(), canExecute)
  {
    _execute = execute;
  }

  public Task ExecuteAsync()
  {
    _execute();
  }
}

然后在单元测试中使用 await command.ExecuteAsync() 而不是 command.Execute()