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 Task
s are handled during test? Or a limitation of Moq? My test methods have this signature:
[Test]
public async Task Test_Name()
{
}
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With