ChatGPT解决这个技术问题 Extra ChatGPT

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

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

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 方法......

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


S
Stephen Cleary

您应该避免使用 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:await让任务观察异常;如果你在没有 await 的情况下调用它,那么任何失败都会被忽略。
R
Reed Copsey

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

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

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


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

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

[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();
}

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


仅仅因为您的 lookupTask 已完成,并不意味着正在测试的方法(DoStuff? 或 DoLookupCommand?)已完成运行。任务完成运行但 IsSearchShowing 尚未设置为 false 的可能性很小,在这种情况下,您的断言将失败。
证明这一点的一种简单方法是在将 IsSearchShowing 设置为 false 之前放置 Thread.Sleep(2000)
R
Richard Ev

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


B
BalintN

您可以使用 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() ?
V
Velimir

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

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

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

和用法:

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

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


B
Billy Jake O'Connor

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

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

configuration 是什么?
@Dementic 一个例子?或者更具体地说,是具有返回任务的成员 InitializeAsync 的对象实例。
d
datchung

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

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

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