【问题标题】:Await a Async Void method call for unit testing等待 Async Void 方法调用以进行单元测试
【发布时间】:2019-08-28 04:26:54
【问题描述】:

我有一个看起来像这样的方法:

private async void DoStuff(long idToLookUp)
{
    IOrder order = await orderService.LookUpIdAsync(idToLookUp);   

    // Close the search
    IsSearchShowing = false;
}    

//Other stuff in case you want to see it
public DelegateCommand<long> DoLookupCommand{ get; set; }
ViewModel()
{
     DoLookupCommand= new DelegateCommand<long>(DoStuff);
}    

我正在尝试像这样对其进行单元测试:

[TestMethod]
public void TestDoStuff()
{
    //+ Arrange
    myViewModel.IsSearchShowing = true;

    // container is my Unity container and it setup in the init method.
    container.Resolve<IOrderService>().Returns(orderService);
    orderService = Substitute.For<IOrderService>();
    orderService.LookUpIdAsync(Arg.Any<long>())
                .Returns(new Task<IOrder>(() => null));

    //+ Act
    myViewModel.DoLookupCommand.Execute(0);

    //+ Assert
    myViewModel.IsSearchShowing.Should().BeFalse();
}

在我完成模拟 LookUpIdAsync 之前调用了我的断言。在我的正常代码中,这正是我想要的。但是对于我的单元测试,我不希望这样。

我正在从使用 BackgroundWorker 转换为 Async/Await。使用后台工作人员,这可以正常工作,因为我可以等待 BackgroundWorker 完成。

但似乎没有办法等待 async void 方法...

如何对这个方法进行单元测试?

【问题讨论】:

    标签: c# .net unit-testing async-await


    【解决方案1】:

    你应该避免async void。仅将 async void 用于事件处理程序。 DelegateCommand (逻辑上)是一个事件处理程序,所以你可以这样做:

    // Use [InternalsVisibleTo] to share internal methods with the unit test project.
    internal async Task DoLookupCommandImpl(long idToLookUp)
    {
      IOrder order = await orderService.LookUpIdAsync(idToLookUp);   
    
      // Close the search
      IsSearchShowing = false;
    }
    
    private async void DoStuff(long idToLookUp)
    {
      await DoLookupCommandImpl(idToLookup);
    }
    

    并对其进行单元测试:

    [TestMethod]
    public async Task TestDoStuff()
    {
      //+ Arrange
      myViewModel.IsSearchShowing = true;
    
      // container is my Unity container and it setup in the init method.
      container.Resolve<IOrderService>().Returns(orderService);
      orderService = Substitute.For<IOrderService>();
      orderService.LookUpIdAsync(Arg.Any<long>())
                  .Returns(new Task<IOrder>(() => null));
    
      //+ Act
      await myViewModel.DoLookupCommandImpl(0);
    
      //+ Assert
      myViewModel.IsSearchShowing.Should().BeFalse();
    }
    

    我推荐的答案在上面。但是如果你真的想测试一个async void 方法,你可以用我的AsyncEx library 来做:

    [TestMethod]
    public void TestDoStuff()
    {
      AsyncContext.Run(() =>
      {
        //+ Arrange
        myViewModel.IsSearchShowing = true;
    
        // container is my Unity container and it setup in the init method.
        container.Resolve<IOrderService>().Returns(orderService);
        orderService = Substitute.For<IOrderService>();
        orderService.LookUpIdAsync(Arg.Any<long>())
                    .Returns(new Task<IOrder>(() => null));
    
        //+ Act
        myViewModel.DoLookupCommand.Execute(0);
      });
    
      //+ Assert
      myViewModel.IsSearchShowing.Should().BeFalse();
    }
    

    但此解决方案会在其生命周期内更改您的视图模型的 SynchronizationContext

    【讨论】:

    • 这行得通。但我宁愿不必为我所有的异步 void 使用两种方法。我想出了一个办法(至少在我的情况下)。如果您有兴趣,请参阅我对这个问题的回答。
    • 很好 - 你已经创建了 github.com/btford/zone.js ZoneJS 的 C# 前身。
    • 在 DoStuff(long idToLookUp) 中等待 DoLookupCommandImpl(idToLookup) 有什么好处?如果它不等待就被调用怎么办?
    • @jacekbe: awaiting 任务观察到异常;如果你在没有await 的情况下调用它,那么任何失败都会被忽略。
    【解决方案2】:

    async void 方法本质上是一种“一劳永逸”的方法。没有办法取回完成事件(没有外部事件等)。

    如果您需要对此进行单元测试,我建议您改为使用async Task 方法。然后您可以在结果上调用Wait(),它会在方法完成时通知您。

    但是,这种编写的测试方法仍然不起作用,因为您实际上并没有直接测试DoStuff,而是测试包装它的DelegateCommand。您需要直接测试此方法。

    【讨论】:

    • 我无法将其更改为返回 Task,因为 DelegateCommand 不允许这样做。
    • 在我的代码周围有一个单元测试脚手架非常重要。如果不能进行单元测试,我可能必须让所有(重要的)“异步无效”方法使用 BackgroundWorker。
    • @Vaccano BackgroundWorker 也会发生同样的事情 - 您只需将其设为 async Task 而不是 async void,然后等待任务...
    • @Vaccano 你应该没有async void 方法(除了事件处理程序)。如果async void方法中出现异常,你将如何处理?
    • 我想出了一种让它工作的方法(至少在这种情况下)。如果您有兴趣,请参阅我对这个问题的回答。
    【解决方案3】:

    我想出了一种方法来进行单元测试:

    [TestMethod]
    public void TestDoStuff()
    {
        //+ Arrange
        myViewModel.IsSearchShowing = true;
    
        // container is my Unity container and it setup in the init method.
        container.Resolve<IOrderService>().Returns(orderService);
        orderService = Substitute.For<IOrderService>();
    
        var lookupTask = Task<IOrder>.Factory.StartNew(() =>
                                      {
                                          return new Order();
                                      });
    
        orderService.LookUpIdAsync(Arg.Any<long>()).Returns(lookupTask);
    
        //+ Act
        myViewModel.DoLookupCommand.Execute(0);
        lookupTask.Wait();
    
        //+ Assert
        myViewModel.IsSearchShowing.Should().BeFalse();
    }
    

    这里的关键是,因为我是单元测试,所以我可以在我想让我的异步调用(在我的异步 void 内)返回的任务中替换。然后,我只需确保任务已完成,然后再继续。

    【讨论】:

    • 仅仅因为你的lookupTask已经完成,并不意味着被测方法(DoStuff?或DoLookupCommand?)已经完成运行。任务完成运行的可能性很小,但 IsSearchShowing 尚未设置为 false,在这种情况下,您的断言将失败。
    • 证明这一点的简单方法是在将IsSearchShowing 设置为false 之前输入Thread.Sleep(2000)
    【解决方案4】:

    我知道的唯一方法是将您的 async void 方法转换为 async Task 方法

    【讨论】:

      【解决方案5】:

      您可以使用 AutoResetEvent 来暂停测试方法,直到异步调用完成:

      [TestMethod()]
      public void Async_Test()
      {
          TypeToTest target = new TypeToTest();
          AutoResetEvent AsyncCallComplete = new AutoResetEvent(false);
          SuccessResponse SuccessResult = null;
          Exception FailureResult = null;
      
          target.AsyncMethodToTest(
              (SuccessResponse response) =>
              {
                  SuccessResult = response;
                  AsyncCallComplete.Set();
              },
              (Exception ex) =>
              {
                  FailureResult = ex;
                  AsyncCallComplete.Set();
              }
          );
      
          // Wait until either async results signal completion.
          AsyncCallComplete.WaitOne();
          Assert.AreEqual(null, FailureResult);
      }
      

      【讨论】:

      • 任何 AsyncMethodToTest 类示例?
      • 为什么不直接使用 Wait() ?
      【解决方案6】:

      提供的答案测试命令而不是异步方法。如上所述,您还需要另一个测试来测试该异步方法。

      花了一些时间处理类似问题后,我发现只需同步调用即可在单元测试中测试异步方法很容易:

          protected static void CallSync(Action target)
          {
              var task = new Task(target);
              task.RunSynchronously();
          }
      

      及用法:

      CallSync(() => myClass.MyAsyncMethod());
      

      测试在这一行等待并在结果准备好后继续,因此我们可以在之后立即断言。

      【讨论】:

        【解决方案7】:

        改变你的方法来返回一个Task,你可以使用Task.Result

        bool res = configuration.InitializeAsync(appConfig).Result;
        Assert.IsTrue(res);
        

        【讨论】:

        • configuration 是什么?
        • @Dementic 一个例子?或者更具体地说,一个对象的实例,它具有返回任务的成员 InitializeAsync。
        【解决方案8】:

        我遇到了类似的问题。就我而言,解决方案是在 .Returns(...) 的起订量设置中使用 Task.FromResult,如下所示:

        orderService.LookUpIdAsync(Arg.Any<long>())
            .Returns(Task.FromResult(null));
        

        另外,Moq 也有一个ReturnsAysnc(...) 方法。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2013-02-27
          相关资源
          最近更新 更多