akash dumbre
akash dumbre

Reputation: 21

Wait for void async method to complete in unit test case

These are my business engine's methods. Upload is calling internally async method UploadAsync()

public void Upload(Stream data)
{  
   //Some logic
   //Call private async method
   UploadAsync(data);
}

private async void UploadAsync(Object data)
{
    await Task.Run(() =>
    {
               using (var factory = new DataRepositoryFactoryObject<IAllCommandRepository>(DataRepositoryFactory))
               {
                   factory.Repository.UploadData(data);
               }
           }
    );
}

This is unit test case for Upload() method

[TestMethod, TestCategory("Unit")]
public void Upload_ValidData_Success()
{
    //other logic to setup mock repository
    //call public method which calls async method
    engine.Upload(data);

   _mockAllCommandRepository.Verify(x => x.Upload(It.Is<Object>(t => t != null)), Times.Once);
}

This test case is getting failed occasioally at verify method with following exception:

016-10-06T19:25:20.4982657Z ##[error]Expected invocation on the mock once, but was 0 times: x =>  x.Upload(It.Is<Object>(t => t != null)), Times.Once);

As per my analysis verify method gets called before calling async method. This can be reason of failure of this test case. How can I verify that a method was called on a mock when the method itself is called in a delegate passed to Task.Run? By time mock.Verify is called the Task still hasn't executed. Could anyone please provide some solution so that this test case will pass each and every time

Upvotes: 2

Views: 4043

Answers (2)

tonicsoft
tonicsoft

Reputation: 1808

One of the main benefits of unit tests is that they help you design better code. Most of the time, when you are struggling to write a test, it is telling you that your code needs to be improved, not your test.

In this case the issue is using a void return type with an async method, something which Stephen Cleary advises against. Indeed, one of the reasons he cites in his article is:

Async void methods are difficult to test.

I think one of his most convincing arguments against async void methods is this:

When the return type is Task, the caller knows it’s dealing with a future operation; when the return type is void, the caller might assume the method is complete by the time it returns.

In case you're not convinced yet, here's another article which contains a couple of other reasons to avoid async void.

In summary, you should change your method to return a Task. Then you will be able to wait on it in the test.

private async Task UploadAsync(Object data)
{
    return Task.Run(() => {
           using (var factory = new DataRepositoryFactoryObject<IAllCommandRepository>(DataRepositoryFactory))
           {
               factory.Repository.UploadData(data);
           }
    });
}

and the test...

[TestMethod, TestCategory("Unit")]
public void Upload_ValidData_Success()
{
    //other logic to setup mock repository
    //call public method which calls async method
    engine.Upload(data).Wait();

   _mockAllCommandRepository.Verify(x => x.Upload(It.Is<Object>(t => t != null)), Times.Once);
}

Upvotes: 2

Stephen Cleary
Stephen Cleary

Reputation: 456507

As others have noted, the best solution is to make the method return Task. Task-returning async methods are more easily testable:

private async Task UploadAsync(Object data)
{
  await Task.Run(() =>
  {
    using (var factory = new DataRepositoryFactoryObject<IAllCommandRepository>(DataRepositoryFactory))
    {
      factory.Repository.UploadData(data);
    }
  });
}

private async void Upload(Object data)
{
  await UploadAsync(data);
}

Then your unit test can be:

[TestMethod, TestCategory("Unit")]
public async Task Upload_ValidData_Success()
{
  //other logic to setup mock repository
  //call public method which calls async method
  await engine.UploadAsync(data);

  _mockAllCommandRepository.Verify(x => x.Upload(It.Is<Object>(t => t != null)), Times.Once);
}

However, if for some reason you do need to test an async void method, it's possible to do by using my AsyncContext type:

[TestMethod, TestCategory("Unit")]
public void Upload_ValidData_Success()
{
  //other logic to setup mock repository
  //call public method which calls async method
  AsyncContext.Run(() => engine.Upload(data));

  _mockAllCommandRepository.Verify(x => x.Upload(It.Is<Object>(t => t != null)), Times.Once);
}

Side note: In an ideal scenario, I'd recommend adding an UploadDataAsync to your repository, and then remove the Task.Run.

Upvotes: 4

Related Questions