【问题标题】:Unit test sleep durations polly wait retry policy单元测试睡眠持续时间 polly 等待重试策略
【发布时间】:2020-10-27 17:13:54
【问题描述】:

我想编写一个单元测试来查看执行是否在指定的持续时间内处于休眠状态。我遇到了SystemClock,它是Polly.Utilities 的一部分,但我正在寻找类似于此处WaitAndRetrySpecs 中提到的Polly 单元测试的实现,看起来像这样

 [Fact]
 public void Should_sleep_for_the_specified_duration_each_retry_when_specified_exception_thrown_same_number_of_times_as_there_are_sleep_durations()
 {
      var totalTimeSlept = 0;

      var policy = Policy
            .Handle<DivideByZeroException>()
            .WaitAndRetry(new[]
            {
               1.Seconds(),
               2.Seconds(),
               3.Seconds()
            });

      SystemClock.Sleep = span => totalTimeSlept += span.Seconds;

      policy.RaiseException<DivideByZeroException>(3);

      totalTimeSlept.Should()
                      .Be(1 + 2 + 3);
  }

目前我的政策看起来像

var customPolicy = Policy
                     .Handle<SqlException>(x => IsTransientError(x))
                     .WaitAndRetryAsync(
                      3,
                      (retryAttempt) => getSleepDurationByRetryAtempt(retryAttempt)
                     );

我想测试政策的总体睡眠时间。对于每次重试尝试 [1,2,3],睡眠持续时间为 [1,2,3]。 在所有 3 次重试之后,总睡眠持续时间应为 1 + 2 + 3 = 6。此测试与上面链接中提到的 Polly 规范非常相似。

问题:如何为 customPolicy 编写单元测试来测试类似于 polly 规范的总睡眠持续时间。我想查看编写单元测试的实现或说明。

【问题讨论】:

    标签: c# nunit mstest xunit polly


    【解决方案1】:

    您可以通过使用onRetry 函数来实现这一点。

    为简单起见,让我定义 IsTransientErrorGetSleepDurationByRetryAttempt 方法,如下所示:

    public TimeSpan GetSleepDurationByRetryAttempt(int attempt) => TimeSpan.FromSeconds(attempt);
    public bool IsTransientError(SqlException ex) => true;
    

    顺便说一句,您可以通过避免(不必要的)匿名 lambda 来缩短您的策略定义:

    var customPolicy = Policy
        .Handle<SqlException>(IsTransientError)
        .WaitAndRetryAsync(3, GetSleepDurationByRetryAttempt)
    

    所以,回到 onRetry。 an overload 具有以下签名:Action&lt;Exception, TimeSpan, Context&gt;。这里的第二个参数是睡眠时长。

    我们需要做的就是在这里提供一个函数,它会累积睡眠时长。

    var totalSleepDuration = TimeSpan.Zero;
    ...
    onRetry: (ex, duration, ctx) => { totalSleepDuration = totalSleepDuration.Add(duration); }
    

    让我们把所有这些放在一起:

    [Fact]
    public async Task GivenACustomSleepDurationProvider_WhenIUseItInARetryPolicy_ThenTheAccumulatedDurationIsAsExpected()
    {
        //Arrange
        var totalSleepDuration = TimeSpan.Zero;
        var customPolicy = Policy
            .Handle<SqlException>(IsTransientError)
            .WaitAndRetryAsync(3, GetSleepDurationByRetryAttempt,
                onRetry: (ex, duration, ctx) => { totalSleepDuration = totalSleepDuration.Add(duration); }
            );
    
        //Act
        Func<Task> actionWithRetry = async() => await customPolicy.ExecuteAsync(() => throw new SqlException());
        
        //Assert
        _ = await Assert.ThrowsAsync<SqlException>(actionWithRetry);
        Assert.Equal(6, totalSleepDuration.Seconds);
    }
    

    更新 #1:减少延迟并引入理论

    根据您的要求,使用不同参数运行相同的测试用例可能是有意义的。这就是TheoryInlineData 可以帮助您的地方:

    [Theory]
    [InlineData(3, 600)]
    [InlineData(4, 1000)]
    [InlineData(5, 1500)]
    public async Task GivenACustomSleepDurationProvider_WhenIUseItInARetryPolicy_ThenTheAccumulatedDurationIsAsExpected(int retryCount, int expectedTotalSleepInMs)
    {
        //Arrange
        var totalSleepDuration = TimeSpan.Zero;
        var customPolicy = Policy
            .Handle<SqlException>(IsTransientError)
            .WaitAndRetryAsync(retryCount, GetSleepDurationByRetryAttempt,
                onRetry: (ex, duration, ctx) => { totalSleepDuration = totalSleepDuration.Add(duration); }
            );
    
        //Act
        Func<Task> actionWithRetry = async () => await customPolicy.ExecuteAsync(() => throw new SqlException());
    
        //Assert
        _ = await Assert.ThrowsAsync<SqlException>(actionWithRetry);
        Assert.Equal(TimeSpan.FromMilliseconds(expectedTotalSleepInMs), totalSleepDuration);
    }
    
    public static TimeSpan GetSleepDurationByRetryAttempt(int attempt) => TimeSpan.FromMilliseconds(attempt * 100);
    

    更新 #2:通过 Context 传递 TimeSpan

    为了实现TimeSpana bit more type-safe的传输和检索,我们可以为此创建两个扩展方法:

    public static class ContextExtensions
    {
        private const string Accumulator = "DurationAccumulator";
    
        public static Context SetAccumulator(this Context context, TimeSpan durationAccumulator)
        {
            context[Accumulator] = durationAccumulator;
            return context;
        }
    
        public static TimeSpan? GetAccumulator(this Context context)
        {
            if (!context.TryGetValue(Accumulator, out var ts))
                return null;
    
            if (ts is TimeSpan accumulator) 
                return accumulator;
    
            return null;
        }
    }
    

    我们还可以提取Policy创建逻辑:

    private AsyncPolicy GetCustomPolicy(int retryCount)
        => Policy
            .Handle<SqlException>(IsTransientError)
            .WaitAndRetryAsync(retryCount, GetSleepDurationByRetryAttempt,
                onRetry: (ex, duration, ctx) =>
                {
                    var totalSleepDuration = ctx.GetAccumulator();
                    if (!totalSleepDuration.HasValue) return;
                    totalSleepDuration = totalSleepDuration.Value.Add(duration);
                    ctx.SetAccumulator(totalSleepDuration.Value);
                });
    

    现在让我们把所有这些放在一起(再一次):

    [Theory]
    [InlineData(3, 600)]
    [InlineData(4, 1000)]
    [InlineData(5, 1500)]
    public async Task GivenACustomSleepDurationProvider_WhenIUseItInARetryPolicy_ThenTheAccumulatedDurationIsAsExpected(
            int retryCount, int expectedTotalSleepInMs)
    {
        //Arrange
        var totalSleepDuration = TimeSpan.Zero;
        var customPolicy = GetCustomPolicy(retryCount);
        var context = new Context().SetAccumulator(totalSleepDuration);
    
        //Act
        Func<Task> actionWithRetry = async () => await customPolicy.ExecuteAsync(ctx => throw new SqlException(), context);
    
        //Assert
        _ = await Assert.ThrowsAsync<SqlException>(actionWithRetry);
        var accumulator = context.GetAccumulator();
        Assert.NotNull(accumulator);
        Assert.Equal(TimeSpan.FromMilliseconds(expectedTotalSleepInMs), accumulator.Value);
    }
    

    【讨论】:

    • 这是有道理的,我想我可能想到的唯一改变是将 totalSleepDuration 添加到上下文并在您提到的重载中访问它,因为我的策略是在不同的类(有点工厂)中定义的,而不是在单元中测试方法。我会试一试,让你知道。
    • @user14530157 我已经更新回答以通过Context 传递totalSleepDuration。请检查一下。
    【解决方案2】:

    测试不工作,因为您使用的是 WaitAndRetryAsync 而不是 WaitAndRetry。我遇到了类似的问题并通过使用修复了

     SystemClock.SleepAsync = (s, cts) =>
            {
                totalTimeSlept += s.Seconds;
                return Task.CompletedTask;
            };
    

    【讨论】:

      猜你喜欢
      • 2019-05-05
      • 1970-01-01
      • 2022-07-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-09-24
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多