Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mocking Async Methods

We are writing unit tests for async code using MSTest and Moq.

So we have some code that looks something like :

var moq = new Mock<Foo>();
moq.Setup(m => m.GetAsync())
   .Returns(Task.FromResult(10));

Or like this on projects that have a more recent version of Moq

var moq = new Mock<Foo>();
moq.Setup(m => m.GetAsync())
   .ReturnsAsync(10);

Looking at the Moq implementation of ReturnsAsync :

public static IReturnsResult<TMock> ReturnsAsync<TMock, TResult>(this IReturns<TMock, Task<TResult>> mock, TResult value) where TMock : class
{
  TaskCompletionSource<TResult> completionSource = new TaskCompletionSource<TResult>();
  completionSource.SetResult(value);
  return mock.Returns(completionSource.Task);
}

Both methods seem to be the same under the hood. Both create a TaskCompletionSource, call SetResult and return the Task

So far so good.

But short running async methods are optimized to act synchronously. This seems to imply that TaskCompletionSource is always synchronous, which would also seem to suggest that context handling and any related issues that can occur would never happen.

So if we had some code that was doing some async no-no's, like mixing awaits, Wait() and Result, that these problems would not be detected in unit testing.

Would there be any advantage to creating an extension method which always yields control? Something like :

public async Task<T> ReturnsYieldingAsync<T>(T result)
{
    await Task.Yield();
    return result;
}

In this case we would have a method that is guaranteed to execute asynchronously.

The perceived advantage would be detecting bad asynchronous code. For example, it could catch any deadlocking or exception swallowing during unit testing.

I am not 100% sure this is the case, so I would really be interested in hearing what the community has to say.

like image 555
swestner Avatar asked Oct 30 '22 11:10

swestner


1 Answers

When I first started talking about testing asynchronous code four years ago (!), I would encourage developers to test along the async axis for their mocks. That is, test along the result axis (success, fail) as well as the async axis (sync, async).

Over time, though, I've loosened up on this. When it comes to testing my own code, I really only test the synchronous success/fail paths, unless there's a clear reason to also test the asynchronous success path for that code. I don't bother with asynchronous failure testing at all anymore.

like image 188
Stephen Cleary Avatar answered Nov 15 '22 04:11

Stephen Cleary