Недавно я писал метод async, который вызывает внешний долго работающий метод async, поэтому я решил передать CancellationToken, разрешающий отмену. Метод может быть вызван одновременно.
Реализация сочетает методы экспоненциальной отсрочки и тайм-аута, описанные в Stephen Cleary в книге Concurrency in C# Cookbook следующим образом;
/// <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();
}
}
}
}
Интересно, должен ли я написать модульный тест, который явно утверждает, что внутренняя задача barService.GetBarAsync()
отменяется всякий раз, когда отменяется FooAsync()
. Если да, то как реализовать это чисто?
Кроме того, должен ли я игнорировать детали реализации и просто проверить, что касается клиента/вызывающего, как описано в сводке метода (панель обновлена, триггеры отмены OperationCanceledException
, триггеры тайм-аута TimeoutException
).
Если нет, должен ли я намочить ноги и начать реализовывать модульные тесты для следующих случаев:
- Тестирование потокобезопасности (монитор запускается только одним потоком за раз)
- Тестирование механизма повтора
- Проверка сервера не залита
- Тестирование, возможно, даже обычное исключение распространяется на вызывающую сторону