Asynchronous module testing method: how to explicitly state that an internal task has been canceled

I recently wrote an asynchronous call method that calls an external long asynchronous method, so I decided to pass a CancellationToken, which allows to cancel. The method can be called at the same time.

The implementation combined the exponential deferral and timeout methods described in Stephen Cleary 's book in the 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 unit test, which explicitly states that the barService.GetBarAsync() internal task is canceled whenever FooAsync() is canceled. If so, how to implement it cleanly?

In addition, whether to ignore implementation details and just check what the client / caller is, as described in the method summary (line updated, operationCanceledException cancellation triggers, TimeoutException timeout TimeoutException ).

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

  • Testing is thread safe (monitor is only purchased one thread at a time)
  • Repeat mechanism testing
  • Server testing not flooded
  • Testing, perhaps even a regular exception applies to the caller
+5
source share
2 answers

I wonder if I should write unit test, which explicitly states that the barService.GetBarAsync () internal task is canceled whenever FooAsync () is canceled.

It would be easier to write a test that claims that the cancel token transferred to GetBarAsync is canceled whenever the cancel token transferred to FooAsync is canceled.

For asynchronous unit testing, my choice signal is TaskCompletionSource<object> for asynchronous signals and ManualResetEvent for synchronous signals. Since GetBarAsync is asynchronous, I would use asynchronous 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 these signals to create a kind of "lock".


Side note: in my own code, I never write retry logic. I use Polly , which is fully async compatible and thoroughly tested. This will reduce the semantics that should be tested to:

  • CT is passed (indirectly) to the service method, resulting in an OperationCanceledException at startup.
  • There is also a timeout, resulting in a TimeoutException .
  • Doing mutex'ed.

(1) will be executed as above. (2) and (3) are less easy to test (for correct tests requiring either MS Fakes or abstractions for time / mutex). Of course, there is a certain point of profit reduction when it comes to unit testing, and it is up to you how far you want to go.

+2
source

Thanks Steven Cleary for nodding Polly a try again. Perhaps, interest in future readers, all the functionality in the source code of the source code can now be built from ready-made Polly primitives, which are already tested for one:

  • Timeout policy for timeout using a timeout cancellation token (including combining with a cancellation token provided by the user)
  • Throttling / Throttling Policy for Parallelization
  • WaitAndRetry for retry, including cancellation while waiting
  • PolicyWrap to merge.

All Polly policies are fully tested for performance , compatible with synchronization and asynchrony, thread-safe for parallel executions, and have end-to-end cancellation support.

So, the intention of the source code can be achieved something like this:

 Policy retry = Policy.Handle<WhateverExceptions>().WaitAndRetryAsync(RetryLimit, retryAttempt => TimeSpan.FromMilliseconds(250 * Math.Pow(2, retryAttempt))); Policy mutex = Policy.BulkheadAsync(1); Policy timeout = Policy.TimeoutAsync(/* define overall timeout */); bar = await timeout.WrapAsync(retry).WrapAsync(mutex).ExecuteAsync(ct => barService.GetBarAsync(barId, ct), cancellationToken); 

I will add some comments about unit testing (OP's original question) to Steven's comments (much more important), answer that.

+2
source

Source: https://habr.com/ru/post/1258324/


All Articles