Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unit testing async method: How to explicitly assert that the internal task was cancelled

I was recently writing an async method that calls a external long running async method so I decided to pass CancellationToken enabling cancellation. The method can be called concurrently.

Implementation has combined exponential backoff and timeout techniques described in Stephen Cleary's book Concurrency in C# Cookbook as follows;

/// <summary>
/// Sets bar
/// </summary>
/// <param name="cancellationToken">The cancellation token that cancels the operation</param>
/// <returns>A <see cref="Task"/> representing the task of setting bar value</returns>
/// <exception cref="OperationCanceledException">Is thrown when the task is cancelled via <paramref name="cancellationToken"/></exception>
/// <exception cref="TimeoutException">Is thrown when unable to get bar value due to time out</exception>
public async Task FooAsync(CancellationToken cancellationToken)
{
    TimeSpan delay = TimeSpan.FromMilliseconds(250);
    for (int i = 0; i < RetryLimit; i++)
    {
        if (i != 0)
        {
            await Task.Delay(delay, cancellationToken);
            delay += delay; // Exponential backoff
        }

        await semaphoreSlim.WaitAsync(cancellationToken); // Critical section is introduced for long running operation to prevent race condition

        using (CancellationTokenSource cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
        {
            cancellationTokenSource.CancelAfter(TimeSpan.FromMilliseconds(Timeout));
            CancellationToken linkedCancellationToken = cancellationTokenSource.Token;

            try
            {
                cancellationToken.ThrowIfCancellationRequested();
                bar = await barService.GetBarAsync(barId, linkedCancellationToken).ConfigureAwait(false);

                break;
            }
            catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
            {
                if (i == RetryLimit - 1)
                {
                    throw new TimeoutException("Unable to get bar, operation timed out!");
                }

                // Otherwise, exception is ignored. Will give it another try
            }
            finally
            {
                semaphoreSlim.Release();
            }
        }
    }
}

I wonder if I should write a unit test that explicitly asserts that the internal task barService.GetBarAsync() is cancelled whenever FooAsync() is cancelled. If so how to implement it cleanly?

On top of that, should I ignore implementation details and just test what client/caller is concerned as described in method summary (bar is updated, cancel triggers OperationCanceledException, timeout triggers TimeoutException).

If not, should I get my feet wet and start implementing unit tests for following cases:

  1. Testing it is thread-safe (monitor acquired only by single thread at a time)
  2. Testing the retry mechanism
  3. Testing the server is not flooded
  4. Testing maybe even a regular exception is propagated to caller
like image 349
Saro Taşciyan Avatar asked Oct 17 '16 10:10

Saro Taşciyan


People also ask

How do I end async method?

You can cancel an asynchronous operation after a period of time by using the CancellationTokenSource. CancelAfter method if you don't want to wait for the operation to finish.

How do you test asynchronous methods?

This illustrates the first lesson from the async/await conceptual model: To test an asynchronous method's behavior, you must observe the task it returns. The best way to do this is to await the task returned from the method under test.

What is asynchronous testing?

Asynchronous code doesn't execute directly within the current flow of code. This might be because the code runs on a different thread or dispatch queue, in a delegate method, or in a callback, or because it's a Swift function marked with async . XCTest provides two approaches for testing asynchronous code.

Can NUnit tests be async?

NUnit supports out of the box asynchronous methods by wrapping properly executing them in an asynchronous context and unwrapping eventual exception raised by the code itself or by failed assertions. To make sure the runner properly waits for the completion of the asynchronous tests, these cannot be async void methods.


1 Answers

I wonder if I should write a unit test that explicitly asserts that the internal task barService.GetBarAsync() is cancelled whenever FooAsync() is cancelled.

It would be easier to write a test that asserts that the cancellation token passed to GetBarAsync is cancelled whenever the cancellation token passed to FooAsync is cancelled.

For asynchronous unit testing, my signal of choice is TaskCompletionSource<object> for asynchronous signals and ManualResetEvent for synchronous signals. Since GetBarAsync is asynchronous, I'd use the asynchronous one, e.g.,

var cts = new CancellationTokenSource(); // passed into FooAsync
var getBarAsyncReady = new TaskCompletionSource<object>();
var getBarAsyncContinue = new TaskCompletionSource<object>();
bool triggered = false;
[inject] GetBarAsync = async (barId, cancellationToken) =>
{
  getBarAsyncReady.SetResult(null);
  await getBarAsyncContinue.Task;
  triggered = cancellationToken.IsCancellationRequested;
  cancellationToken.ThrowIfCancellationRequested();
};

var task = FooAsync(cts.Token);
await getBarAsyncReady.Task;
cts.Cancel();
getBarAsyncContinue.SetResult(null);

Assert(triggered);
Assert(task throws OperationCanceledException);

You can use signals like this to create a kind of "lock-step".


Side note: in my own code, I never write retry logic. I use Polly, which is fully async-compatible and thoroughly tested. That would reduce the semantics that need to be tested down to:

  1. The CT is passed through (indirectly) to the service method, resulting in OperationCanceledException when triggered.
  2. There is also a timeout, resulting in TimeoutException.
  3. Execution is mutex'ed.

(1) would be done just like the above. (2) and (3) are less easy to test (for proper tests, requiring either MS Fakes or abstractions for time/mutex). There is definitely a point of diminishing returns when it comes to unit testing, and it's up to you how far you want to go.

like image 103
Stephen Cleary Avatar answered Sep 30 '22 06:09

Stephen Cleary