简体   繁体   English

单元测试异步方法:如何显式断言内部任务被取消

[英]Unit testing async method: How to explicitly assert that the internal task was cancelled

I was recently writing an async method that calls a external long running async method so I decided to pass CancellationToken enabling cancellation. 我最近编写了一个异步方法,调用外部长时间运行的异步方法,所以我决定通过CancellationToken启用取消。 The method can be called concurrently. 该方法可以同时调用。

Implementation has combined exponential backoff and timeout techniques described in Stephen Cleary 's book Concurrency in C# Cookbook as follows; 实现结合了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();
            }
        }
    }
}

I wonder if I should write a unit test that explicitly asserts that the internal task barService.GetBarAsync() is cancelled whenever FooAsync() is cancelled. 我不知道我应该写一个单元测试,明确断言,内部任务barService.GetBarAsync()无论何时取消FooAsync()被取消。 If so how to implement it cleanly? 如果是这样,如何干净利落地实施?

On top of that, should I ignore implementation details and just test what client/caller is concerned as described in method summary (bar is updated, cancel triggers OperationCanceledException , timeout triggers TimeoutException ). 最重要的是,我应该忽略实现细节,只测试方法摘要中描述的客户端/调用者(条形更新,取消触发器OperationCanceledException ,超时触发TimeoutException )。

If not, should I get my feet wet and start implementing unit tests for following cases: 如果没有,我应该弄湿自己的脚并开始对以下情况进行单元测试:

  1. Testing it is thread-safe (monitor acquired only by single thread at a time) 测试它是线程安全的(一次只能通过单个线程获取监视器)
  2. Testing the retry mechanism 测试重试机制
  3. Testing the server is not flooded 测试服务器没有被淹没
  4. Testing maybe even a regular exception is propagated to caller 测试甚至可以将常规异常传播给调用者

I wonder if I should write a unit test that explicitly asserts that the internal task barService.GetBarAsync() is cancelled whenever FooAsync() is cancelled. 我想知道是否应该编写一个单元测试,明确断言每当FooAsync()被取消时内部任务barService.GetBarAsync()被取消。

It would be easier to write a test that asserts that the cancellation token passed to GetBarAsync is cancelled whenever the cancellation token passed to FooAsync is cancelled. 这将是更容易编写一个测试,断言传递给取消标记 GetBarAsync每当传递给取消标记被取消FooAsync被取消。

For asynchronous unit testing, my signal of choice is TaskCompletionSource<object> for asynchronous signals and ManualResetEvent for synchronous signals. 对于异步单元测试,我选择的信号是异步信号的TaskCompletionSource<object>和同步信号的ManualResetEvent Since GetBarAsync is asynchronous, I'd use the asynchronous one, eg, 由于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);

You can use signals like this to create a kind of "lock-step". 您可以使用这样的信号来创建一种“锁定步骤”。


Side note: in my own code, I never write retry logic. 旁注:在我自己的代码中,我从不写重试逻辑。 I use Polly , which is fully async -compatible and thoroughly tested. 我使用Polly ,它是完全async兼容和彻底测试的。 That would reduce the semantics that need to be tested down to: 这将减少需要测试的语义:

  1. The CT is passed through (indirectly) to the service method, resulting in OperationCanceledException when triggered. CT(间接)传递给服务方法,触发时导致OperationCanceledException
  2. There is also a timeout, resulting in TimeoutException . 还有一个超时,导致TimeoutException
  3. Execution is mutex'ed. 执行是互斥的。

(1) would be done just like the above. (1)就像上面那样完成。 (2) and (3) are less easy to test (for proper tests, requiring either MS Fakes or abstractions for time/mutex). (2)和(3)不太容易测试(对于正确的测试,需要MS Fakes或抽象时间/互斥量)。 There is definitely a point of diminishing returns when it comes to unit testing, and it's up to you how far you want to go. 在单元测试方面,肯定会有一个收益递减点,而这取决于你想要走多远。

Thanks to Stephen Cleary for the nod to Polly retry. 感谢Stephen Cleary向Polly重新点头。 Perhaps of interest to future readers, all the functionality in the original poster's code sample could now be built from ready-made Polly primitives which are already unit-tested: 未来读者可能感兴趣的是,原始海报代码示例中的所有功能现在都可以使用已经过单元测试的现成Polly原型构建:

All Polly policies are fully unit-tested , sync and async-compatible, thread-safe for concurrent executions, and have pass-through cancellation support. 所有Polly策略都经过完全单元测试 ,同步和异步兼容,并发执行的线程安全,并具有传递取消支持。

So, the intention of the original code could be achieved something like: 因此,原始代码的意图可以实现如下:

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'll add some comments about unit-testing (the OP's original question) to the comments to Stephen's (much more relevant) answer to that. 我将添加一些关于单元测试的评论(OP的原始问题)给Stephen的答案(更为相关)。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM