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完成之前返回?
链接的博客文章中的GetIncompleteTask扩展方法使用,因此引入另一个线程(和竞争条件).Task.Run
该CurrentThreadTaskScheduler会只为工作async void方法,如果所有的任务都已经完成,这是你正是避免使用ReturnsAsyncIncomplete.
这是发生了什么:
Execute跑进来CurrentThreadTaskScheduler.这await是一个在线程池上运行的不完整的任务.此时,StartNew任务完成.您要做的是对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方法而不是自定义等待线程池.