簡體   English   中英

單元測試異步方法:如何顯式斷言內部任務被取消

[英]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 )。

如果沒有,我應該弄濕自己的腳並開始對以下情況進行單元測試:

  1. 測試它是線程安全的(一次只能通過單個線程獲取監視器)
  2. 測試重試機制
  3. 測試服務器沒有被淹沒
  4. 測試甚至可以將常規異常傳播給調用者

我想知道是否應該編寫一個單元測試,明確斷言每當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兼容和徹底測試的。 這將減少需要測試的語義:

  1. CT(間接)傳遞給服務方法,觸發時導致OperationCanceledException
  2. 還有一個超時,導致TimeoutException
  3. 執行是互斥的。

(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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM