等待 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 之前调用我的断言。在我的正常代码中,这正是我想要的。但是对于我的单元测试,我不希望出现这种情况。

我正在从使用 BackoundWorker 转换到 Async/wait。对于后台工作者,这个操作是正确的,因为我可以等待后台工作者完成。

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

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

122303 次浏览

An async void method is essentially a "fire and forget" method. There is no means of getting back a completion event (without an external event, etc).

If you need to unit test this, I would recommend making it an async Task method instead. You can then call Wait() on the results, which will notify you when the method completes.

However, this test method as written would still not work, as you're not actually testing DoStuff directly, but rather testing a DelegateCommand which wraps it. You would need to test this method directly.

You should avoid async void. Only use async void for event handlers. DelegateCommand is (logically) an event handler, so you can do it like this:

// 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);
}

and unit test it as:

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

My recommended answer is above. But if you really want to test an async void method, you can do so with my 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();
}

But this solution changes the SynchronizationContext for your view model during its lifetime.

I figured out a way to do it for unit testing:

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

The key here is that because I am unit testing I can substitute in the task I want to have my async call (inside my async void) to return. I then just make sure the task has completed before I move on.

You can use an AutoResetEvent to halt the test method until the async call completes:

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

The provided answer tests the command and not the async method. As mentioned above you'll need another test to test that async method as well.

After spending some time with a similar problem i found an easy wait to test an async method in a unit test by just calling in synchronously:

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

and the usage:

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

The test waits on this line and continues after the result is ready so we can assert immediately afterwards.

The only way I know is to turn your async void method to async Task method

Change your method to return a Task and you can use Task.Result

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

I had a similar issue. In my case, the solution was to use Task.FromResult in the moq setup for .Returns(...) like so:

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

Alternatively, Moq also has a ReturnsAysnc(...) method.

I ended up with a different approach, something that was suggested in https://learn.microsoft.com/en-us/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming#avoid-async-void

All your async void method does is call this async Task method internally.

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;
}

And then, instead of calling the async void method in my test, I called the async Task method.

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


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


//+ Assert


}