8

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).
BatteryBackupUnit
  • 12,934
  • 1
  • 42
  • 68
  • 2
    If `_keepAliveTask` is cancelled and awaited, it should throw an `OperationCancelledException`, shouldn't it? Im not too sure what you're actually trying to do. You want to ensure `LogOutIfPossible` is awaited? – Yuval Itzchakov Jan 19 '15 at 12:45
  • What are you actually trying to test? that `EndSession` completes? – i3arnon Jan 19 '15 at 12:45
  • @l3arnon that `EndSession` only completes once `_keepAliveTask` completes – BatteryBackupUnit Jan 19 '15 at 12:46
  • 1
    @YuvalItzchakov Yes i think that's a possible solution. Have the task return a result (or throw an exception in case there's no result) and verifying the "wrapping" task is returning that same result (throwing that exception). Thank you! – BatteryBackupUnit Jan 19 '15 at 12:47
  • What testing framework do you use? MS Test supports async test methods. – Krumelur Jan 19 '15 at 12:56
  • do you have any control over `_keepAliveTask`? – i3arnon Jan 19 '15 at 12:58
  • @Krumelur it's not about the test framework supporting async test methods or not. I'm using NUnit which supports `[Test]public async Task MyTestMethdo()` - and i'm using it a lot. – BatteryBackupUnit Jan 19 '15 at 13:01
  • @l3arnon: yes i do have full control on how to create and run the `_keepAliveTask`. – BatteryBackupUnit Jan 19 '15 at 13:01
  • @BatteryBackupUnit, perhaps, I misunderstood what you are trying to achive here, so I'm deleting my answer. – noseratio Jan 20 '15 at 23:35

2 Answers2

4

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.

i3arnon
  • 113,022
  • 33
  • 324
  • 344
0
  1. Use TaskCompletionSource and set its result at a known time.
  2. Verify that before setting the result, the await on EndSession hasn't completed.
  3. Verify that after setting the result, the await on EndSession has completed.

A simplified version could look like the following (using nunit):

[Test]
public async Task VerifyTask()
{
    var tcs = new TaskCompletionSource<bool>();
    var keepAliveTask = tcs.Task;

    // verify pre-condition
    Assert.IsFalse(keepAliveTask.IsCompleted);

    var waitTask = Task.Run(async () => await keepAliveTask);

    tcs.SetResult(true);

    await waitTask;

    // verify keepAliveTask has finished, and as such has been awaited
    Assert.IsTrue(keepAliveTask.IsCompleted);
    Assert.IsTrue(waitTask.IsCompleted); // not needed, but to make a point
}

You can also add a short delay at the waitTask to ensure any synchronous execution would be faster, something like:

var waitTask = Task.Run(async () =>
{
    await Task.Delay(1);
    await keepAliveTask;
 });

And if you don't trust your unit test framework to deal correctly with async, you can set a completed flag as part of the waitTask, and check for that in the end. Something like:

bool completed = false;
var waitTask = Task.Run(async () =>
{
    await Task.Delay(1);
    await keepAliveTask;
    completed = true;
 });

 // { .... }

 // at the end of the method
 Assert.IsTrue(completed);
thoean
  • 1,106
  • 8
  • 15
  • This is basically what i've described with *observing effects instead of verifying calls*. It's not reliable. To achieve sufficient reliability the test has to take long. Waiting a second (or more) is not really acceptable in a unit test. Unless one can run them in parallel then it wouldn't be that big of an issue. – BatteryBackupUnit Jan 19 '15 at 18:38