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:
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.
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.
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.
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.
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:
OperationCanceledException
when triggered.TimeoutException
.(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.
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