在MVVM中自动测试异步命令

时间:2017-08-25 09:03:52

标签: c# wpf unit-testing mvvm integration-testing

我有一个异步Command类,如下所示:

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

这个测试有更好的解决方案吗?也许某些东西依赖于它实际需要多长时间,并且不估计等待多长时间。

3 个答案:

答案 0 :(得分:2)

你可以参考@StephenCleary的一篇好博文: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();
  }
}

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

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

但是您可以在链接的博文中找到更多高级变体。您可以在代码中一直使用IAsyncCommand,以便进行测试。您使用的MVVM框架也会很高兴,因为该接口基于ICommand

答案 1 :(得分:2)

  

这个测试有更好的解决方案吗?也许某些东西依赖于它实际需要多长时间,并且不能估计等待多长时间。

当然:让命令等待awaitasync void方法是不好的做法,意味着用于事件处理程序。

Mvvm.AsyncReactiveUI中有可用的命令,您可以使用或至少查看以供参考。

答案 2 :(得分:1)

如果您要检查状态,则应该使用SpinWait.SpinUntil

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

e.g。

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

var informationViewModel = findProductViewModel.ProductViewModel.ProductInformationViewModel;

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