MVVM 中异步命令的自动化测试

Automated test for a async Command in MVVM

我有一个异步 Command class,像这样:

public AsyncDelegateCommand(Func<Task> execute, Func<bool> canExecute)
{
    this.execute = execute;
    this.canExecute = canExecute;
}

public virtual bool CanExecute(object parameter)
{
    if(executing)
        return false;
    if(canExecute == null)
        return true;
    return canExecute();
}

public async void Execute(object parameter) // Notice "async void"
{
    executing = true;
    CommandManager.InvalidateRequerySuggested();
    if(parameter != null && executeWithParameter != null)
        await executeWithParameter(parameter);
    else if(execute != null)
        await execute();
    executing = false;
    CommandManager.InvalidateRequerySuggested();
}

它的名字是这样的:

FindProductCommand = new AsyncDelegateCommand(TryToFindProduct, () => CanFindProduct() && connector.HasConnection);

private async Task TryToFindProduct()
{
    //code
}

当我进行单元测试时,它工作得很好,因为我从任务中立即返回。

然而,在编写我的集成测试时,我 运行 遇到了麻烦。我无法等待 Execute,因为它是 void,而且我无法将其更改为 Task。我最终这样做了:/

findProductViewModel.FindProductCommand.Execute(null);
Thread.Sleep(2000);

var informationViewModel = findProductViewModel.ProductViewModel.ProductInformationViewModel;

Assert.AreEqual("AFG00", informationViewModel.ProductGroup);

这个测试有没有更好的解决方案?可能是一些依赖实际需要多长时间的东西,并没有估计要等待多长时间。

如果你有一个状态要检查,你应该只使用 SpinWait.SpinUntil:

它会比 Thread.Sleep 可靠得多,因为它允许您在继续之前检查条件是否为真。

例如

findProductViewModel.FindProductCommand.Execute(null);
SpinWait.SpinUntil(() => findProductViewModel.ProductViewModel.ProductInformationViewModel != null, 5000);

var informationViewModel = findProductViewModel.ProductViewModel.ProductInformationViewModel;

Assert.AreEqual("AFG00", informationViewModel.ProductGroup);

您可以参考@StephenCleary 的一篇不错的博客post:https://msdn.microsoft.com/en-us/magazine/dn630647.aspx
通常要避免 async void,因此他为异步命令引入了一个新接口(及其基本实现):IAsyncCommand。此接口包含您可以在测试中等待的方法 async Task ExecuteAsync(object parameter)

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

public abstract class AsyncCommandBase : IAsyncCommand
{
  public abstract bool CanExecute(object parameter);
  public abstract Task ExecuteAsync(object parameter);

  public async void Execute(object parameter)
  {
    await ExecuteAsync(parameter);
  }

  public event EventHandler CanExecuteChanged
  {
    add { CommandManager.RequerySuggested += value; }
    remove { CommandManager.RequerySuggested -= value; }
  }

  protected void RaiseCanExecuteChanged()
  {
    CommandManager.InvalidateRequerySuggested();
  }
}

这种异步命令的最简单实现如下所示:

public class AsyncCommand : AsyncCommandBase
{
  private readonly Func<Task> _command;

  public AsyncCommand(Func<Task> command)
  {
    _command = command;
  }

  public override bool CanExecute(object parameter)
  {
    return true;
  }

  public override Task ExecuteAsync(object parameter)
  {
    return _command();
  }
}

但是您可以在链接的博客中找到更高级的变体 post。你可以在你的代码中一路使用IAsyncCommands,这样你就可以测试它们了。您使用的 MVVM 框架也会很高兴,因为该接口基于 ICommand.

Is there a better solution for this test? Maybe something that is reliant on how long it actually takes, and doesn't estimate how long to wait.

当然:使命令可等待并 await 它。 async void 方法是不好的做法, 用于事件处理程序。

Mvvm.Async and ReactiveUI 中有可用的可等待命令,您可以使用或至少看一看以供参考。