单元测试 Polly - 检查是否在超时/错误时触发重试策略

Unit test Polly - Check whether the retry policy is triggered on timeout / error

问题 stmt:由于服务器问题,我有一个服务有时会从 graphql 获取结果,该服务可能会抛出 500 错误

解决方案:为了解决上述问题,我需要编写一个重试逻辑来在发生超时时重试服务。

障碍:我不知道如何断言给定逻辑是否按规定调用服务三次。感谢任何帮助。

我创建了一个重试策略,如果给定的客户端在一段时间后超时则重试。

public override void ConfigureServices(IServiceCollection services)
{
  services.AddHttpClient<GraphQueryService>(Constants.PPQClient)
        .ConfigureHttpClient(client =>
        {
            client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            client.Timeout = TimeSpan.FromMilliseconds(Constants.ClientTimeOut);
        }).AddRetryPolicy();
}

重试逻辑:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Polly;

namespace PA.Com.Integration
{
    public static class HttpClientBuilderExtensions
    {
        public static IHttpClientBuilder AddRetryPolicy(this IHttpClientBuilder builder)
        {
            var serviceProvider = builder.Services.BuildServiceProvider();

            var options = serviceProvider.GetRequiredService<IOptions<RetryOptions>>();

            return builder.AddTransientHttpErrorPolicy(b => b.WaitAndRetryAsync(new[]
            {
                options.Value.RetryDelay1,
                options.Value.RetryDelay2,
                options.Value.RetryDelay3
            }));
        }
    }
}

我是单元测试的新手我相信我调用了代码来检查超时但不确定如何断言它是否在超时时被调用了三次。

我试过的单元测试:

[Fact]
public async Task Check_Whether_Given_Policy_Executed_OnTimeout()
{
    // Given / Arrange 
    IServiceCollection services = new ServiceCollection();

    bool retryCalled = false;

    HttpStatusCode codeHandledByPolicy = HttpStatusCode.InternalServerError;

   var data =  services.AddHttpClient<GraphQueryService>(Constants.PPQClient)
            .ConfigureHttpClient(client =>
            {
                client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
                client.Timeout = TimeSpan.FromMilliseconds(Constants.ClientTimeOut);
            }).AddRetryPolicy()
    .AddHttpMessageHandler(() => new StubDelegatingHandler(codeHandledByPolicy));

 //Need to Check the retry logic is called three times. Not sure How to continue after this.

    Assert.Equal(codeHandledByPolicy, HttpStatusCode.InternalServerError);
    Assert.True(retryCalled);
}

遗憾的是,您无法创建单元测试来确保您的策略设置正确。例如,在您设置重试次数和休眠持续时间后,您将无法查询它们。

阅读 Polly 的源代码后,我找到了一个解决方案,但它非常脆弱,因为它依赖于 private 字段。我已经提出 a ticket,它将在 V8 中解决。 (何时发布存在很大的不确定性。)


那么,你能做什么?好吧,您可以在模拟下游 http 服务的地方编写集成测试。为此,我选择了 WireMock.Net 库。

我在下游系统上创建了两个抽象:

FlawlessService

internal abstract class FlawlessServiceMockBase
{
    protected readonly WireMockServer server;
    private readonly string route;

    protected FlawlessServiceMockBase(WireMockServer server, string route)
    {
        this.server = server;
        this.route = route;
    }

    public virtual void SetupMockForSuccessResponse(IResponseBuilder expectedResponse = null, 
        HttpStatusCode expectedStatusCode = HttpStatusCode.OK)
    {
        server.Reset();

        var endpointSetup = Request.Create().WithPath(route).UsingGet();
        var responseSetup = expectedResponse ?? Response.Create().WithStatusCode(expectedStatusCode);

        server.Given(endpointSetup).RespondWith(responseSetup);
    }
}

FautlyService

internal abstract class FaultyServiceMockBase
{
    protected readonly WireMockServer server;
    protected readonly IRequestBuilder endpointSetup;
    protected readonly string scenario;

    protected FaultyServiceMockBase(WireMockServer server, string route)
    {
        this.server = server;
        this.endpointSetup = Request.Create().WithPath(route).UsingGet();
        this.scenario = $"polly-setup-test_{this.GetType().Name}";
    }

    public virtual void SetupMockForFailedResponse(IResponseBuilder expectedResponse = null,
        HttpStatusCode expectedStatusCode = HttpStatusCode.InternalServerError)
    {
        server.Reset();

        var responseSetup = expectedResponse ?? Response.Create().WithStatusCode(expectedStatusCode);

        server.Given(endpointSetup).RespondWith(responseSetup);
    }

    public virtual void SetupMockForSlowResponse(ResilienceSettings settings, string expectedResponse = null)
    {
        server.Reset();

        int higherDelayThanTimeout = settings.HttpRequestTimeoutInMilliseconds + 500;

        server
            .Given(endpointSetup)
            .InScenario(scenario)
            //NOTE: There is no WhenStateIs
            .WillSetStateTo(1)
            .WithTitle(Common.Constants.Stages.Begin)
            .RespondWith(DelayResponse(higherDelayThanTimeout, expectedResponse));

        for (var i = 1; i < settings.HttpRequestRetryCount; i++)
        {
            server
                .Given(endpointSetup)
                .InScenario(scenario)
                .WhenStateIs(i)
                .WillSetStateTo(i + 1)
                .WithTitle($"{Common.Constants.Stages.RetryAttempt} #{i}")
                .RespondWith(DelayResponse(higherDelayThanTimeout, expectedResponse));
        }

        server
            .Given(endpointSetup)
            .InScenario(scenario)
            .WhenStateIs(settings.HttpRequestRetryCount)
            //NOTE: There is no WillSetStateTo
            .WithTitle(Common.Constants.Stages.End)
            .RespondWith(DelayResponse(1, expectedResponse));
    }

    private static IResponseBuilder DelayResponse(int delay) => Response.Create()
        .WithDelay(delay)
        .WithStatusCode(200);

    private static IResponseBuilder DelayResponse(int delay, string response) => 
        response == null 
            ? DelayResponse(delay) 
            : DelayResponse(delay).WithBody(response);
}

使用这两个 classes,您可以模拟行为良好和不良的下游系统。

  • WireMock 服务器将 运行 在本地的指定端口上(稍后会提供详细信息)并侦听 GET 请求的可配置路由
  • ResilienceSettings 只是一个简单的助手 class 来存储超时和重试策略的配置值
  • 在服务器出现故障的情况下,我们定义了一个scenario,它基本上是一个请求-响应对序列
    • 为了测试重试策略,您可以指定中间步骤的数量
    • 在所有不成功的(中间)请求之后,WireMock 服务器将自身转换为 End 状态(WithTitle(Common.Constants.Stages.End)),这就是您可以在集成测试中查询的内容

这是一个简单的测试,它会针对慢速下游系统发出请求(重试)。失败了好几次,最后都成功了

[Fact]
public async Task GivenAValidInout_AndAServiceWithSlowProcessing_WhenICallXYZ_ThenItCallsTheServiceSeveralTimes_AndFinallySucceed()
{
    //Arrange - Proxy request
    HttpClient proxyApiClient = proxyApiInitializer.CreateClient();

    //Arrange - Service
    var xyzSvc = new FaultyXYZServiceMock(xyzServer.Value);
    xyzSvc.SetupMockForSlowResponse(resilienceSettings);

    //Act
    var actualResult = await CallXYZAsync(proxyApiClient);

    //Assert - Response
    const HttpStatusCode expectedStatusCode = HttpStatusCode.OK;
    actualResult.StatusCode.ShouldBe(expectedStatusCode);

    //Assert - Resilience Policy
    var logsEntries = xyzServer.Value.FindLogEntries(
        Request.Create().WithPath(Common.Constants.Routes.XYZService).UsingGet());
    logsEntries.Last().MappingTitle.ShouldBe(Common.Constants.Stages.End);
}

请注意 proxyApiInitializerWebApplicationFactory<Startup> 派生的实例 class。

最后,这是初始化 WireMock 服务器的方法

private static Lazy<WireMockServer> xyzServer;

public ctor()
{
   xyzServer = xyzServer ?? InitMockServer(API.Constants.EndpointConstants.XYZServiceApi);
}

private Lazy<WireMockServer> InitMockServer(string lookupKey)
{
    string baseUrl = proxyApiInitializer.Configuration.GetValue<string>(lookupKey);
    return new Lazy<WireMockServer>(
        WireMockServer.Start(new FluentMockServerSettings { Urls = new[] { baseUrl } }));
}