Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Async callback on mocked object not awaiting

I am attempting to mock a complicated situation for unit testing:

_mockController = new Mock<IController>();
_mockController.Setup(tc => tc.Interrupt(It.IsAny<Func<Task>>()))
    .Callback<Func<Task>>(async f => await f.Invoke());

Where IController has a void method Interrupt(Func<Task>> f), which queues some work to be done.

My objects under test do call Interrupt(), and I can verify the call like so:

_mockController.Verify(tc => tc.Interrupt(It.IsAny<Func<Task>>()), Times.Once);

...but when the argument Func<Task> is invoked in the callback, the await keyword is not respected in the Task: the execution of the test continues before the Task finishes (despite the await in the callback). One symptom of this is that adding an await Task.Delay(1000) in the Interrupt() Task argument turns a passing test into a failing test.

Is this behavior due to a nuance of how threads or Tasks are handled during test? Or a limitation of Moq? My test methods have this signature:

[Test]
public async Task Test_Name()
{
}
like image 606
jdslepp Avatar asked Apr 12 '16 20:04

jdslepp


1 Answers

Callback can't return a value, and thus shouldn't be used to execute asynchronous code (or synchronous code that needs to return a value). Callback is a kind of "injection point" that you can hook into to examine the parameters passed to the method, but not modify what it returns.

If you want a lambda mock, you can just use Returns:

_mockController.Setup(tc => tc.Interrupt(It.IsAny<Func<Task>>()))
    .Returns(async f => await f());

(I'm assuming Interrupt returns Task).

the execution of the test continues before the Task finishes (despite the await in the callback).

Yes, since Callback can't return a value, it's always typed as Action/Action<...>, so your async lambda ends up being an async void method, with all the problems that brings (as described in my MSDN article).

Update:

Interrupt() is actually a void method: what it does is queue the function (the argument) until the IController can be bothered to stop what it is doing. Then it invokes the function--which returns a Task--and awaits that Task

You can mock that behavior then, if this would work:

List<Func<Task>> queue = new List<Func<Task>>();
_mockController.Setup(tc => tc.Interrupt(It.IsAny<Func<Task>>()))
    .Callback<Func<Task>>(f => queue.Add(f));

... // Code that calls Interrupt

// Start all queued tasks and wait for them to complete.
await Task.WhenAll(queue.Select(f => f()));

... // Assert / Verify
like image 108
Stephen Cleary Avatar answered Oct 05 '22 12:10

Stephen Cleary