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


77

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

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之前,将调用我的assert。用我的普通代码,这就是我想要的。但是对于我的单元测试,我不想要那个。

我正在使用BackgroundWorker转换为Async / Await。使用后台工作程序,此程序可以正常运行,因为我可以等待BackgroundWorker完成。

但是似乎没有一种方法可以等待异步void方法...

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

Answers:


69

你应该避免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库进行测试

[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在其生命周期内更改for view模型。


这会起作用。但是我宁愿不必为我的所有异步虚空使用两种方法。我想出了一种方法(至少就我而言)。如果您有兴趣,请参阅我对这个问题的回答。
瓦卡诺

很好-您已经创建了github.com/btford/zone.js ZoneJS的C#前身。
Jochen van Wylick 2014年

在DoStuff(long idToLookUp)中等待DoLookupCommandImpl(idToLookup)有什么好处?如果不等待就调用该怎么办?
jacekbe

2
@jacekbe:await执行任务时会观察到异常;如果不使用调用它await,那么任何故障都会被忽略。
斯蒂芬·克莱里

59

一种async void方法本质上是一种“发射后不管”的方法。没有办法退回完成事件(没有外部事件等)。

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

但是,这种书面测试方法仍然无法使用,因为您实际上并没有DoStuff直接进行测试,而是测试了DelegateCommand其包装的。您将需要直接测试此方法。


1
我不能将其更改为返回Task,因为DelegateCommand不允许这样做。
瓦卡诺

在我的代码周围有一个单元测试支架非常重要。如果不能对它们进行单元测试,则可能必须使所有(重要的)“异步无效”方法都使用BackgroundWorker。
瓦卡诺

1
@Vaccano BackgroundWorker也会发生相同的事情-您只需要使其变为an,async Task而不是async void,然后等待任务...
Reed Copsey

@Vaccano您应该没有任何async void方法(事件处理程序除外)。如果async void方法中引发了异常,您将如何处理它?
svick

我想出了一种使其工作的方法(至少在这种情况下)。如果您有兴趣,请参阅我对这个问题的回答。
瓦卡诺

18

我想出了一种用于单元测试的方法:

[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,在这种情况下,您的声明将失败。
dcastro 2013年

一种简单的证明方法是将其Thread.Sleep(2000)设置IsSearchShowing 为false。
dcastro 2013年


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类示例吗?
Kiquenet 2014年

2
为什么不只使用Wait()呢?
Teoman shipahi's

5

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

花了一些时间解决类似的问题后,我发现只需等待同步即可轻松地在单元测试中测试异步方法:

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

和用法:

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

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



0

我有一个类似的问题。就我而言,解决方案是Task.FromResult在moq设置中使用,.Returns(...)例如:

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

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.