如何在 Polly 重试策略中捕获最后一次试验结果?

How to capture last trial result in Poly retry policy?

我正在努力扩展 .NET Core 中的 TestMethod 属性。我将 Polly 库用于重试逻辑以及外部超时策略。

我想要一个可以重试调用 ITestMethod 直到它通过的辅助方法。我不介意重试的次数。但我会设置一个超时时间,必须在该时间内完成。如果delegate在超时时间内执行成功,就可以了。但是如果出现超时异常,我还是想要失败的结果值(最后一次迭代的结果)而不是TimeOutRejectedException或者return类型的默认值

下面是我的扩展测试方法属性class:

public sealed class GuaranteedPassTestMethodAttribute : TestMethodAttribute
{
    /// <inheritdoc/>
    public override TestResult[] Execute(ITestMethod testMethod)
    {
        return ExecuteTestTillSuccess(testMethod);
    }

    private TestResult[] ExecuteTestTillSuccess(ITestMethod testMethod)
    {
        var gracefulTestRun =
            TestExecutionHelper.ExecuteTestTillPassAsync(
                () => TestInvokeDelegate(testMethod));

        return gracefulTestRun.Result;
    }

    private Task<TestResult[]> TestInvokeDelegate(ITestMethod testMethod)
    {
        TestResult[] result = null;
        var thread = new Thread(() => result = Invoke(testMethod));
        thread.Start();
        thread.Join();

        return Task.FromResult(result);
    }
}

下面是我使用 Polly 的 TestExecutionHelper:

internal static class TestExecutionHelper
{
    private static readonly Func<TestResult[], bool> TestFailurePredicate =
        results => results != null &&
                   results.Length == 1 &&
                   results.First().Outcome != UnitTestOutcome.Passed;

    internal static async Task<TestResult[]> ExecuteTestTillPassAsync(
        Func<Task<TestResult[]>> testInvokeDelegate,
        int delayBetweenExecutionInMs = 3000,
        int timeoutInSeconds = 60 * 10)
    {
        var timeoutPolicy = Policy.TimeoutAsync<TestResult[]>(timeoutInSeconds);
        var retryPolicy = Policy.HandleResult<TestResult[]>(TestFailurePredicate)
                                .WaitAndRetryAsync(int.MaxValue, x => TimeSpan.FromMilliseconds(delayBetweenExecutionInMs));
        var testRunPolicy = timeoutPolicy.WrapAsync(retryPolicy);
        return await testRunPolicy.ExecuteAsync(testInvokeDelegate);
    }
}

使用此设置,我要么获得通过的测试方法执行,要么 TimeOutRejectedException 获得失败的测试。我想在重试后捕获失败的测试 TestResult

测试用例

假设我们有以下测试:

[TestClass]
public class UnitTest1
{
    private static int counter;
    [GuaranteedPassTestMethod]
    public async Task TestMethod1()
    {
        await Task.Delay(1000);
        Assert.Fail($"Failed for {++counter}th time");
    }
}

我使用了 static 变量(称为 counter)来更改每个测试的输出 运行。

属性

我已经使用 Task.Run:

简化了您的属性代码
public sealed class GuaranteedPassTestMethodAttribute : TestMethodAttribute
{
    public override TestResult[] Execute(ITestMethod testMethod)
        => TestExecutionHelper
            .ExecuteTestTillPassAsync(
                async () => await Task.Run(
                    () => testMethod.Invoke(null)))
            .GetAwaiter().GetResult();
}

我还将 .Result 更改为 GetAwaiter().GetResult() 以获得更好的异常处理。

  • 如果您不熟悉此模式,请阅读 this topic

策略

我引入了一个名为 Results 的累加器变量,我在其中捕获所有测试 运行 的结果。

internal static class TestExecutionHelper
{
    private static readonly List<TestResult> Results = new List<TestResult>();
    private static readonly Func<TestResult, bool> TestFailurePredicate = result =>
    { 
        Results.Add(result);
        return result != null && result.Outcome != UnitTestOutcome.Passed;
    };

    internal static async Task<TestResult[]> ExecuteTestTillPassAsync(
        Func<Task<TestResult>> testInvokeDelegate,
        int delayBetweenExecutionInMs = 3000,
        int timeoutInSeconds = 1 * 10)
    {
        var timeoutPolicy = Policy.TimeoutAsync<TestResult>(timeoutInSeconds);
        var retryPolicy = Policy.HandleResult(TestFailurePredicate)
            .WaitAndRetryForeverAsync(_ => TimeSpan.FromMilliseconds(delayBetweenExecutionInMs));
        var testRunPolicy = timeoutPolicy.WrapAsync(retryPolicy);

        try { await testRunPolicy.ExecuteAsync(testInvokeDelegate); }
        catch (TimeoutRejectedException) { } //Suppress

        return Results.ToArray();
    }
}
  • 我在这里使用 WaitAndRetryForeverAsync 而不是 WaitAndRetryAsync(int.MaxValue, ...
  • 我还更改了 testInvokeDelegate 的签名以与界面对齐。

输出

对于我的测试,我已将 timeoutInSeconds 的默认值减少到 10 秒。

 TestMethod1
   Source: UnitTest1.cs line 17

Test has multiple result outcomes
   3 Failed

Results

    1)  TestMethod1
      Duration: 1 sec

      Message: 
        Assert.Fail failed. Failed for 1th time
      Stack Trace: 
        UnitTest1.TestMethod1() line 20
        ThreadOperations.ExecuteWithAbortSafety(Action action)

    2)  TestMethod1
      Duration: 1 sec

      Message: 
        Assert.Fail failed. Failed for 2th time
      Stack Trace: 
        UnitTest1.TestMethod1() line 20
        ThreadOperations.ExecuteWithAbortSafety(Action action)

    3)  TestMethod1
      Duration: 1 sec

      Message: 
        Assert.Fail failed. Failed for 3th time
      Stack Trace: 
        UnitTest1.TestMethod1() line 20
        ThreadOperations.ExecuteWithAbortSafety(Action action)

让我们看看这个测试的时间表运行:

  • 0 >> 1: TestMethod1Delay
  • 1: Assert.Fail
  • 1 >> 4:重试的惩罚
  • 4 >> 5: TestMethod1Delay
  • 5: Assert.Fail
  • 5 >> 8:重试的惩罚
  • 8 >> 9: TestMethod1Delay
  • 9: Assert.Fail
  • 9 >> 12: : 重试惩罚
  • 10:超时开始

这里需要注意一件事:测试持续时间不包含重试的惩罚延迟: