[英]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.