[英]Unit testing async method: How to explicitly assert that the internal task was cancelled
我最近编写了一个异步方法,调用外部长时间运行的异步方法,所以我决定通过CancellationToken启用取消。 该方法可以同时调用。
实现结合了Stephen Cleary 在C#Cookbook中的Concurrency一书中描述的指数退避和超时技术,如下所示;
/// <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
)。
如果没有,我应该弄湿自己的脚并开始对以下情况进行单元测试:
我想知道是否应该编写一个单元测试,明确断言每当FooAsync()被取消时内部任务barService.GetBarAsync()被取消。
这将是更容易编写一个测试,断言传递给取消标记 GetBarAsync
每当传递给取消标记被取消FooAsync
被取消。
对于异步单元测试,我选择的信号是异步信号的TaskCompletionSource<object>
和同步信号的ManualResetEvent
。 由于GetBarAsync
是异步的,我会使用异步的,例如,
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);
您可以使用这样的信号来创建一种“锁定步骤”。
旁注:在我自己的代码中,我从不写重试逻辑。 我使用Polly ,它是完全async
兼容和彻底测试的。 这将减少需要测试的语义:
OperationCanceledException
。 TimeoutException
。 (1)就像上面那样完成。 (2)和(3)不太容易测试(对于正确的测试,需要MS Fakes或抽象时间/互斥量)。 在单元测试方面,肯定会有一个收益递减点,而这取决于你想要走多远。
感谢Stephen Cleary向Polly重新点头。 未来读者可能感兴趣的是,原始海报代码示例中的所有功能现在都可以使用已经过单元测试的现成Polly原型构建:
所有Polly策略都经过完全单元测试 ,同步和异步兼容,并发执行的线程安全,并具有传递取消支持。
因此,原始代码的意图可以实现如下:
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);
我将添加一些关于单元测试的评论(OP的原始问题)给Stephen的答案(更为相关)。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.