【问题标题】:CurrentThreadTaskScheduler does not finish SynchronousCurrentThreadTaskScheduler 未完成同步
【发布时间】:2014-10-21 08:23:15
【问题描述】:

我尝试为视图模型编写单元测试,但在尝试验证 ICommand 两次调用异步方法时卡住了。

我将 Moq 用于我的依赖项。 我是这样设置异步方法的。

this.communicationServiceFake
     .Setup(x => x.WriteParameterAsync(It.IsAny<string>(), It.IsAny<object>()))
     .ReturnsAsyncIncomplete();

扩展 ReturnsAsyncIncomplete 不会在 await 关键字处立即返回,基本上类似于此处找到的内容:Async/Await and code coverage

我使用自己的 TaskSheduler 来确保方法在 Task.Factory.StartNew 返回之前竞争。

Task.Factory.StartNew(() => viewModel.Command.Execute(null), 
    CancellationToken.None, TaskCreationOptions.None, new CurrentThreadTaskScheduler ());

基本上,CurrentThreadTaskScheduler 来自这里:Wait until all Task finish in unit test,看起来像这样:

public class CurrentThreadTaskScheduler : TaskScheduler
{
    protected override void QueueTask(Task task)
    {
        this.TryExecuteTask(task);
    }

    protected override bool TryExecuteTaskInline(Task task, bool wasPreviouslyQueued)
    {
        return this.TryExecuteTask(task);
    }

    protected override IEnumerable<Task> GetScheduledTasks()
    {
        yield break;
    }
}

命令确实调用了以下代码:

await this.communicationService.WriteParameterAsync("Parameter1", true);
await this.communicationService.WriteParameterAsync("Parameter2", true);

然后验证:

  this.communicationServiceFake
       .Verify(t => t.WriteParameterAsync("Parameter1", true), Times.Once);
  this.communicationServiceFake
       .Verify(t => t.WriteParameterAsync("Parameter2", true), Times.Once);

有时它会说第二次通话没有发生。 如果我用 ThreadSleep 替换我的 Task.Factory.StartNew 以确保一切都完成,即使它确实注意到不必要地延迟我的单元测试似乎是正确的,但一切正常。

为什么我的 CurrentThreadTaskScheduler 允许 Task.Factory.StartNew 在我的 Command.Execute 完成之前返回?

【问题讨论】:

  • 我似乎完全不明白你为什么需要使用StartNew
  • 如果我只使用 viewModel.Command.Execute,Command.Execute 在后台有一些未完成的触发和忘记任务,这是尝试使用客户任务等待这些触发并忘记任务调度器。遗憾的是它没有按预期工作。

标签: c# unit-testing task-parallel-library async-await


【解决方案1】:

the linked blog post 中的GetIncompleteTask 扩展方法使用Task.Run,因此引入了另一个线程(和竞争条件)。

CurrentThreadTaskScheduler 仅适用于 async void 方法,前提是所有任务都已完成,这正是您使用 ReturnsAsyncIncomplete避免的。

这是发生了什么:

  1. ExecuteCurrentThreadTaskScheduler 中运行。它awaits 是在线程池上运行的不完整任务。至此,StartNew 任务完成。
  2. 线程池线程完成未完成的任务,并在当前线程(线程池线程)上执行其延续。

您要做的是对具有完全覆盖(即异步)的async void 方法进行单元测试。这当然不容易。

推荐解决方案

使用某种形式的async-aware ICommand,例如我在my MSDN article 中描述的那些。然后你就可以在你的单元测试中使用await viewModel.Command.ExecuteAsync(null),而完全不用自定义任务调度器。

另外,异步模拟真的可以简化;不需要自定义等待者,因为 .NET 框架已经有一个:Task.Yield。所以你可以用一个扩展方法(未经测试)替换所有代码:

public static IReturnsResult<TMock> ReturnsIncompleteAsync<TMock, TResult>(this IReturns<TMock, Task<TResult>> mock, TResult value) where TMock : class
{
  return mock.Returns(async () =>
  {
    await Task.Yield();
    return value;
  });
}

保持异步无效

如果您真的想保留现有的 ICommand 实现,并且想要对 async void 方法进行单元测试,并且想要完整的代码覆盖率,那么您真的需要自定义 SynchronizationContext 而不是自定义TaskScheduler。最简单的方法是安装我的AsyncEx NuGet package 并使用AsyncContext

// Blocks the current thread until all continuations have completed.
AsyncContext.Run(() => viewModel.Command.Execute(null));

另外,我建议您使用上述 Task.Yield 方法,而不是 custom-awaitable-on-a-thread-pool。

【讨论】:

  • 感谢您的出色回答,如果我做对了,IAsyncCommand 仅在我可以将视图模型中的类型更改为 IAsyncCommand 时才提供解决方案,否则我仍然会使用 ICommand 进行单元测试.Execute 又被实现为 async void 了吧?
猜你喜欢
  • 1970-01-01
  • 2020-07-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多