【问题标题】:How to test async void DelegateCommand method containing await with xUnit?如何使用 xUnit 测试包含 await 的 async void DelegateCommand 方法?
【发布时间】:2019-06-13 22:18:07
【问题描述】:

这是我第一次为异步方法编写单元测试。我正在使用 xUnit。我搜索了 SO,但没有得到有希望的结果。我发现但对我不起作用的最好方法是从THIS 示例中实现IAsyncLifetime。对于如何解决此问题的任何提示,我将不胜感激。

目前我所拥有的。在经过测试的虚拟机中,我有一个命令:

public ICommand TestResultsCommand { get; private set; }

并且该命令在VM构造函数中初始化如下:

TestResultsCommand = new DelegateCommand(OnTestResultExecuteAsync);

命令调用方法:

private async void OnTestResultExecuteAsync(object obj)
        {
            TokenSource = new CancellationTokenSource();
            CancellationToken = TokenSource.Token;

            await TestHistoricalResultsAsync();
        }

TestHistoricalResultsAsync 方法的签名如下:

private async Task TestHistoricalResultsAsync()

现在让我们进入单元测试项目。目前在测试类我有一个方法:

[Fact]//testing async void
        public void OnTestResultExecuteAsync_ShouldCreateCancellationTokenSource_True()
        {
            CancellationTokenSource tokenSource = new CancellationTokenSource();
            CancellationToken cancellationToken = tokenSource.Token;
            _viewModel.TestResultsCommand.Execute(null);
            Assert.Equal(cancellationToken.CanBeCanceled, _viewModel.CancellationToken.CanBeCanceled);
            Assert.Equal(cancellationToken.IsCancellationRequested, _viewModel.CancellationToken.IsCancellationRequested);
        }

测试让我例外:

消息:System.NullReferenceException:对象引用未设置为 一个对象的实例。

异常的堆栈跟踪是:

提前感谢您的时间和建议。

【问题讨论】:

  • async void 应该只用于事件处理程序。参考Async/Await - Best Practices in Asynchronous Programming.
  • 还应该重构代码以使其更加可靠,以便您可以正确测试和维护代码。如果没有更好地了解被测类,就没有更多可以建议的了,因为您似乎正在尝试测试私有成员。
  • @Nkosi 但async void 不应该也用于DelegateCommand
  • @Nkosi 好吧,在我的测试方法中,我刚刚执行了命令 async await Task.Run(() => _viewModel.TestResultsCommand.Execute(null)); 并且它起作用了。基于此的解决方案:forums.xamarin.com/discussion/97504/testing-async-commands

标签: c# async-await xunit delegatecommand


【解决方案1】:

problems of async void methods 之一是它们难以测试。对于您的问题,大多数开发人员会执行以下操作之一:

  1. 定义并使用IAsyncCommand 接口。
  2. 将他们的逻辑async Task 公开。
  3. 使用支持异步命令的框架,例如 MvvmCross。

详情请见我的MSDN magazine article on the subject

这是第二种方法的示例:

TestResultsCommand = new DelegateCommand(async () => await OnTestResultExecuteAsync());

public async Task OnTestResultExecuteAsync()
{
  TokenSource = new CancellationTokenSource();
  CancellationToken = TokenSource.Token;

  await TestHistoricalResultsAsync();
}

[Fact]
public async Task OnTestResultExecuteAsync_ShouldCreateCancellationTokenSource_True()
{
  CancellationTokenSource tokenSource = new CancellationTokenSource();
  CancellationToken cancellationToken = tokenSource.Token;
  await _viewModel.OnTestResultExecuteAsync();
  Assert.Equal(cancellationToken.CanBeCanceled, _viewModel.CancellationToken.CanBeCanceled);
  Assert.Equal(cancellationToken.IsCancellationRequested, _viewModel.CancellationToken.IsCancellationRequested);
}

如果您不想仅仅为了单元测试而公开async Task 方法,那么您可以使用某种IAsyncCommand;可以是您自己的(如我的文章中所详述)或来自库(例如 MvvmCross)的一个。这是一个使用 MvvmCross 类型的示例:

public IMvxAsyncCommand TestResultsCommand { get; private set; }

TestResultsCommand = new MvxAsyncCommand(OnTestResultExecuteAsync);

private async Task OnTestResultExecuteAsync() // back to private
{
  TokenSource = new CancellationTokenSource();
  CancellationToken = TokenSource.Token;

  await TestHistoricalResultsAsync();
}

[Fact]
public async Task OnTestResultExecuteAsync_ShouldCreateCancellationTokenSource_True()
{
  CancellationTokenSource tokenSource = new CancellationTokenSource();
  CancellationToken cancellationToken = tokenSource.Token;
  await _viewModel.TestResultsCommand.ExecuteAsync();
  Assert.Equal(cancellationToken.CanBeCanceled, _viewModel.CancellationToken.CanBeCanceled);
  Assert.Equal(cancellationToken.IsCancellationRequested, _viewModel.CancellationToken.IsCancellationRequested);
}

如果您更喜欢IMvxAsyncCommand 方法但不想要MvvmCross 依赖,那么define your own IAsyncCommand and AsyncCommand types 并不难。

【讨论】:

  • 好的,感谢您的反馈。它给了我大局。我将不得不修改我的DelegateCommand 以使用TestResultsCommand = new DelegateCommand(async () => await OnTestResultExecuteAsync());。我之前尝试过这个,但DelegateCommand 不想接受参数,所以我留在了 void。生病改变这一点。谢谢!
  • 再次嗨,我测试了你的方法,包括创建AsyncCommand,但是在测试方法中而不是await _viewModel.OnTestResultExecuteAsync(); 我不得不使用await Task.Run(() => _viewModel.TestResultsCommand.Execute(null));,否则似乎是TestHistoricalResultsAsync 不是运行异步并仍然丢弃相同的异常。
  • await Task.Run 不可能解决这个问题。听起来你有一个竞争条件,Task.Run 使它(几乎)需要足够长的时间。更好的解决方案是修复竞态条件。
  • 老实说,await Task.Run 解决了这个问题。 TestHistoricalResultsAsync 只返回Task,而await Task.Run 是异步执行的,不会抛出异常。您可以在此处找到源代码:github.com/przemyslawbak/Horse_Picker/blob/… 我不确定您所说的修复竞争条件是什么意思?
猜你喜欢
  • 1970-01-01
  • 2022-06-23
  • 2018-06-29
  • 1970-01-01
  • 1970-01-01
  • 2019-07-22
  • 2022-12-17
  • 1970-01-01
  • 2020-07-16
相关资源
最近更新 更多