等待Async Void方法调用进行单元测试

时间:2013-01-07 22:57:03

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

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

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完成。

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

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

8 个答案:

答案 0 :(得分:54)

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

// 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

答案 1 :(得分:50)

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

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

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

答案 2 :(得分:17)

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

[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内)返回的任务。然后,我确保在继续之前完成任务。

答案 3 :(得分: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);
}

答案 4 :(得分:5)

我知道的唯一方法是将[class*="fa-caret-"]{ &:left ? 'top: -45' : 'top: 45'; } 方法转换为async void方法

答案 5 :(得分:4)

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

在花了一些时间处理类似的问题之后,我发现很容易等待在单元测试中通过同步调用来测试异步方法:

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

和用法:

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

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

答案 6 :(得分:0)

更改方法以返回任务,即可使用Task.Result

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

答案 7 :(得分:0)

我有一个类似的问题。在我的情况下,解决方案是在Task.FromResult的最小订量设置中使用.Returns(...),如下所示:

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

或者,Moq也有一种ReturnsAysnc(...)方法。