等待一个异步的void方法调用进行单元测试

90
我有一个看起来像这样的方法:
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时,这个功能是正常的,因为我可以等待BackgroundWorker完成。
但是似乎没有办法等待一个异步的void方法...
我该如何对这个方法进行单元测试?

相关链接:等待一个异步的void方法 - undefined
9个回答

79

您应该避免使用 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库

[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


这个方法可以工作。但我不想为所有我的异步voids都写两个方法。我找到了一种方法来解决它(至少在我的情况下)。如果你感兴趣,可以看看我在这个问题上的答案。 - Vaccano
不错 - 你已经创建了 C# 前身 https://github.com/btford/zone.js/ ZoneJS。 - Jochen van Wylick
在DoStuff(long idToLookUp)中等待DoLookupCommandImpl(idToLookup)有什么好处?如果不等待它会怎样? - jacekbe
2
@jacekbe: await等待任务观察异常;如果您在没有await的情况下调用它,则任何失败都会被静默忽略。 - Stephen Cleary

63
一个async void方法本质上是一个“点火并忘记”的方法。没有办法获得完成事件(除非有外部事件等)。
如果您需要对其进行单元测试,我建议将其改为async Task方法。然后您可以在结果上调用Wait(),它会在该方法完成时通知您。
但是,按照现有的测试方法仍无法起作用,因为您实际上并没有直接测试DoStuff,而是测试封装它的DelegateCommand。您需要直接测试此方法。

1
我无法将它更改为返回Task,因为DelegateCommand不允许这样做。 - Vaccano
在我的代码周围搭建单元测试脚手架非常重要。如果无法进行单元测试,我可能需要让所有(重要的)“异步 void”方法使用BackgroundWorker。 - Vaccano
1
@Vaccano 如果使用BackgroundWorker,同样的情况也会发生-您只需要将其更改为“async Task”而不是“async void”,并等待任务完成即可... - Reed Copsey
@Vaccano 除了事件处理程序之外,您不应该有任何 async void 方法。如果在 async void 方法中引发异常,您将如何处理它? - svick
我找到了一种方法让它工作(至少对于这种情况)。如果您感兴趣,请查看我的答案。 - Vaccano
async voidfire-and-forget更加"火而崩溃"。在async void方法中的任何错误都会在捕获的同步上下文中重新抛出,通常会终止进程。 - undefined

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中)返回的任务。然后,我只需确保任务已完成,然后再继续进行。


1
仅仅因为你的lookupTask已经完成,不代表待测试的方法(是DoStuff还是DoLookupCommand?)也已经完成运行。有一种可能是任务已经完成运行,但是IsSearchShowing还没有被设置为false,这种情况下你的断言会失败。 - dcastro
1
一个简单的方法是在将 IsSearchShowing 设置为 false 之前加上 Thread.Sleep(2000) 来证明这一点。 - dcastro

9

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


5

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

在花费了一些时间解决类似问题后,我发现可以通过同步调用来简单地在单元测试中测试异步方法:

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

用法:

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

测试在此行等待,直到结果准备就绪,因此我们随后可以立即进行断言。

你确定 RunSynchronously() 可以等待 async void 完成吗? - undefined

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
2
为什么不直接使用Wait()函数呢? - Teoman shipahi

2
将你的方法改为返回一个Task,然后你就可以使用Task.Result。"最初的回答"
bool res = configuration.InitializeAsync(appConfig).Result;
Assert.IsTrue(res);

什么是“配置”? - Rafael Herscovici
1
@Dementic 一个例子?或者更具体地说,一个具有成员InitializeAsync并返回任务的对象实例。 - Billy Jake O'Connor

0

我最后采取了一种不同的方法,这是在https://learn.microsoft.com/en-us/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming#avoid-async-void中建议的。

你所有的async void方法所做的就是在内部调用这个async Task方法。

private async void DoStuff(long idToLookUp)
{
    await DoStuffAsync(idLookUp).ConfigureAwait(false);
} 

internal async Task DoStuffAsync(long idToLookUp) //Note: make sure to expose Internal to your test project
{
    IOrder order = await orderService.LookUpIdAsync(idToLookUp);   

    // Close the search
    IsSearchShowing = false;
} 

然后,我在测试中调用了async Task方法,而不是调用async void方法。

[TestMethod]
public async Task TestDoStuff()
{
  //+ Arrange

  //+ Act
  await myViewModel.DoStuffAsync(0);

  //+ Assert

}

-1

我遇到了类似的问题。在我的情况下,解决方案是在moq设置.Returns(...)时使用Task.FromResult

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

另外,Moq还有一个ReturnsAsync(...)方法。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接