Как зафиксировать результат последнего испытания в политике повторных попыток Poly?

Я работаю над расширением атрибута TestMethod в .NET Core. Я использую библиотеку Polly для логики повторных попыток вместе с внешней политикой тайм-аута.

Мне нужен вспомогательный метод, который может повторять вызов ITestMethod, пока он не пройдет. Я не возражаю против количества повторных попыток. Но я поставлю тайм-аут, в течение которого он должен быть завершен. Если делегат успешно выполнен в течение тайм-аута, все в порядке. Но если есть исключение тайм-аута, мне все равно нужно значение неудачного результата (результат последней итерации) вместо TimeOutRejectedException или значения по умолчанию для возвращаемого типа.

Ниже приведен мой расширенный класс атрибутов метода тестирования:

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);
    }
}

Ниже мой TestExecutionHelper, который использует Polly:

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 неудачных тестов даже после повторных попыток.


person Manikanta    schedule 25.01.2021    source источник
comment
Не могли бы вы уточнить этот код Invoke(testMethod)? Насколько я понимаю, testMethod.Invoke существует, но возвращает единственный TestResult.   -  person Peter Csala    schedule 26.01.2021


Ответы (1)


Прецедент

Предположим, у нас есть следующий тест:

[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() для лучшей обработки исключений.

  • Если вы не знакомы с этим шаблоном, прочтите эту тему .

Стратегия

Я ввел аккумуляторную переменную 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: TestMethod1's Delay
  • 1: Assert.Fail
  • 1 ›› 4: Штраф за повторную попытку
  • 4 >> 5: TestMethod1's Delay
  • 5: Assert.Fail
  • 5 ›› 8: Штраф за повторную попытку
  • 8 >> 9: TestMethod1's Delay
  • 9: Assert.Fail
  • 9 ›› 12:: Штраф за повторную попытку
  • 10: Начинается тайм-аут

Здесь следует отметить одну вещь: длительность теста не будет содержать штрафных задержек повторных попыток:

Продолжительность теста

person Peter Csala    schedule 26.01.2021
comment
@Mani Эта версия вам понравилась? - person Peter Csala; 05.02.2021