C# CurrentThreadTaskScheduler未完成同步

C# CurrentThreadTaskScheduler未完成同步,c#,unit-testing,task-parallel-library,async-await,C#,Unit Testing,Task Parallel Library,Async Await,我尝试为视图模型编写一个单元测试,但在尝试验证ICommand两次调用异步方法时遇到了问题 我使用Moq作为我的依赖项。 我这样设置异步方法 this.communicationServiceFake .Setup(x => x.WriteParameterAsync(It.IsAny<string>(), It.IsAny<object>())) .ReturnsAsyncIncomplete(); 基本上,CurrentThreadTask

我尝试为视图模型编写一个单元测试,但在尝试验证ICommand两次调用异步方法时遇到了问题

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

this.communicationServiceFake
     .Setup(x => x.WriteParameterAsync(It.IsAny<string>(), It.IsAny<object>()))
     .ReturnsAsyncIncomplete();
基本上,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;
    }
}
然后验证:

  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完成之前返回

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

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

下面是正在发生的事情:

  • Execute
    CurrentThreadTaskScheduler
    中运行。它等待线程池上运行的未完成任务。此时,
    StartNew
    任务完成
  • 线程池线程完成未完成的任务,并在当前线程(线程池线程)上执行其延续
  • 您试图做的是对一个具有完全覆盖(即异步)的
    异步void
    方法进行单元测试。这当然不容易

    推荐解决方案

    使用某种形式的
    async
    -aware
    ICommand
    ,如我在中所述。然后,您可以在单元测试中使用
    wait viewModel.Command.ExecuteAsync(null)
    ,而不必与自定义任务调度器混在一起

    而且,异步模拟可以真正简化;不需要自定义等待程序,因为.NET framework已经有了一个:
    Task.Yield
    。因此,您可以用一个扩展方法(未测试)替换所有代码:


    另外,我建议您使用上面描述的
    Task.Yield
    方法,而不是定制的线程池。

    我似乎不明白为什么您需要使用
    StartNew
    命令。Execute在后台有一些火和遗忘任务,如果我只使用viewModel.Command.Execute,这些任务还没有完成,这是一种尝试,通过客户任务调度器等待这些触发并忘记任务。遗憾的是,它并没有像预期的那样工作。感谢您提供了一个很好的答案,如果我答对了,IAsyncCommand只提供了一个解决方案,前提是我可以将视图模型中的类型更改为IAsyncCommand,否则我仍然在使用ICommand.Execute进行单元测试,它再次被实现为async void,对吗?
    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);
    
    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;
      });
    }
    
    // Blocks the current thread until all continuations have completed.
    AsyncContext.Run(() => viewModel.Command.Execute(null));