Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Verify that task is being awaited

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 awaiting 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).
like image 281
BatteryBackupUnit Avatar asked Jan 19 '15 12:01

BatteryBackupUnit


1 Answers

If all you want is to verify that EndSession is awaiting _keepAliveTask (and you really have full control over _keepAliveTask) then you can create your own awaitable type instead of Task the signals when it's awaited and check that:

public class MyAwaitable
{
    public bool IsAwaited;
    public MyAwaiter GetAwaiter()
    {
        return new MyAwaiter(this);
    }
}

public class MyAwaiter
{
    private readonly MyAwaitable _awaitable;

    public MyAwaiter(MyAwaitable awaitable)
    {
        _awaitable = awaitable;
    }

    public bool IsCompleted
    {
        get { return false; }
    }

    public void GetResult() {}

    public void OnCompleted(Action continuation)
    {
        _awaitable.IsAwaited = true;
    }
}

Since all you need to await something is that has a GetAwaiter method that returns something with IsCompleted, OnCompleted and GetResult you can use the dummy awaitable to make sure _keepAliveTask is being awaited:

_keepAliveTask = new MyAwaitable();
EndSession();
_keepAliveTask.IsAwaited.Should().BeTrue();

If you use some mocking framework you can instead make Task's GetAwaiter return our MyAwaiter.

like image 165
i3arnon Avatar answered Oct 28 '22 22:10

i3arnon