Reputation: 21
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
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
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