I have the following code which i'd like to test:
private Task _keepAliveTask; // get's assigned by object initializer
public async Task EndSession()
{
_cancellationTokenSource.Cancel(); // cancels the _keepAliveTask
await _logOutCommand.LogOutIfPossible();
await _keepAliveTask;
}
It is important that the EndSession
Task only ends once the `_keepAliveTask' ended. However, I'm struggling to find a way to test it reliably.
Question: How do i unit test the EndSession
method and verify that the Task
returned by EndSession
awaits the _keepAliveTask
.
For demonstration purposes, the unit test could look like that:
public async Task EndSession_MustWaitForKeepAliveTaskToEnd()
{
var keepAliveTask = new Mock<Task>();
// for simplicity sake i slightly differ from the other examples
// by passing the task as method parameter
await EndSession(keepAliveTask);
keepAliveTask.VerifyAwaited(); // this is what i want to achieve
}
Further criterias: - reliable test (always passes when implementation is correct, always fails when implementation is wrong) - cannot take longer than a few milliseconds (it's a unit test, after all).
I have already taken several alternatives into considerations which i'm documenting below:
non-async
method
If there wouldn't be the call to _logOutCommand.LogOutIfPossible() it would be quite simple: i'd just remove the async
and return _keepAliveTask
instead of await
ing it:
public Task EndSession()
{
_cancellationTokenSource.Cancel();
return _keepAliveTask;
}
The unit test would look (simplified):
public void EndSession_MustWaitForKeepAliveTaskToEnd()
{
var keepAliveTask = new Mock<Task>();
// for simplicity sake i slightly differ from the other examples
// by passing the task as method parameter
Task returnedTask = EndSession(keepAliveTask);
returnedTask.Should().be(keepAliveTask);
}
However, there's two arguments against this:
- i have multiple task which need awaiting (i'm considering
Task.WhenAll
further down) - doing so only moves the responsibility to await the task to the caller of
EndSession
. Still will have to test it there.
non-async method, sync over async
Of course, I could do something similar:
public Task EndSession()
{
_cancellationTokenSource.Cancel(); // cancels the _keepAliveTask
_logOutCommand.LogOutIfPossible().Wait();
return _keepAliveTask;
}
But that is a no-go (sync over async). Plus it still has the problems of the previous approach.
non-async
method using Task.WhenAll(...)
Is a (valid) performance improvement but introduces more complexity: - difficult to get right without hiding a second exception (when both fail) - allows parallel execution
Since performance isn't key here i'd like to avoid the extra complexity. Also, previously mentioned issue that it just moves the (verification) problem to the caller of the EndSession
method applies here, too.
observing effects instead of verifying calls
Now of course instead of "unit" testing method calls etc. I could always observe effects. Which is: As long as _keepAliveTask
hasn't ended the EndSession
Task
mustn't end either. But since I can't wait indefinite one has to settle for a timeout. The tests should be fast so a timeout like 5 seconds is a no go. So what I've done is:
[Test]
public void EndSession_MustWaitForKeepAliveTaskToEnd()
{
var keepAlive = new TaskCompletionSource<bool>();
_cancelableLoopingTaskFactory
.Setup(x => x.Start(It.IsAny<ICancelableLoopStep>(), It.IsAny<CancellationToken>()))
.Returns(keepAlive.Task);
_testee.StartSendingKeepAlive();
_testee.EndSession()
.Wait(TimeSpan.FromMilliseconds(20))
.Should().BeFalse();
}
But I really really dislike this approach:
- hard to understand
- unreliable
- or - when it's quite reliable - it takes a long time (which unit tests shouldn't).