CurrentThreadTaskScheduler没有完成Synchronous

Con*_*ole 2 c# unit-testing task-parallel-library async-await

我尝试为View Model编写单元测试,但在尝试验证ICommand调用异步方法两次时,我遇到了问题.

我使用Moq作为我的依赖项.我像这样设置了异步方法.

this.communicationServiceFake
     .Setup(x => x.WriteParameterAsync(It.IsAny<string>(), It.IsAny<object>()))
     .ReturnsAsyncIncomplete();
Run Code Online (Sandbox Code Playgroud)

Extension ReturnsAsyncIncomplete不会立即返回await关键字,基本上类似于以下内容:Async/Await和代码覆盖率

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

Task.Factory.StartNew(() => viewModel.Command.Execute(null), 
    CancellationToken.None, TaskCreationOptions.None, new CurrentThreadTaskScheduler ());
Run Code Online (Sandbox Code Playgroud)

基本上,CurrentThreadTaskScheduler来自这里:等到所有任务在单元测试中完成并且看起来像这样:

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

命令会调用以下代码:

await this.communicationService.WriteParameterAsync("Parameter1", true);
await this.communicationService.WriteParameterAsync("Parameter2", true);
Run Code Online (Sandbox Code Playgroud)

然后验证:

  this.communicationServiceFake
       .Verify(t => t.WriteParameterAsync("Parameter1", true), Times.Once);
  this.communicationServiceFake
       .Verify(t => t.WriteParameterAsync("Parameter2", true), Times.Once);
Run Code Online (Sandbox Code Playgroud)

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

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

Ste*_*ary 6

链接的博客文章中GetIncompleteTask扩展方法使用,因此引入另一个线程(和竞争条件).Task.Run

CurrentThreadTaskScheduler会只为工作async void方法,如果所有的任务都已经完成,这是你正是避免使用ReturnsAsyncIncomplete.

这是发生了什么:

  1. Execute跑进来CurrentThreadTaskScheduler.这await是一个在线程池上运行的不完整的任务.此时,StartNew任务完成.
  2. 线程池线程完成不完整的任务,并在当前线程(线程池线程)上执行其延续.

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

推荐解决方案

使用某种形式的async-aware,ICommand例如我在MSDN文章中描述的那些.然后你可以await viewModel.Command.ExecuteAsync(null)在你的单元测试中使用,而不是乱用自定义任务调度程序.

此外,异步模拟可以真正简化; 因为.NET框架已经有了一个,所以不需要定制的awaiter : 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;
  });
}
Run Code Online (Sandbox Code Playgroud)

保持异步无效

如果你真的想保留现有的ICommand实现,并希望单元测试async void方法,希望完整的代码覆盖,那么你真的需要一个自定义SynchronizationContext而不是自定义TaskScheduler.最简单的方法是安装我的AsyncEx NuGet包并使用AsyncContext:

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

此外,我建议您使用上述Task.Yield方法而不是自定义等待线程池.