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



问题:我有一个从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();
}

RetryLogic:

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

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

单元测试I tried:

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

不幸的是,您不能创建单元测试来确保您的策略已经正确设置。例如,在您设置重试计数和睡眠持续时间(s)之后,您无法查询它们。

阅读Polly的源代码后,我找到了一个解决方案,但它是超级脆弱的,因为它依赖于private字段。我已经提出了一个问题,它将在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);
}

有了这两个类,你就可以模拟好的和坏的下游系统。

  • WireMock服务器将在本地指定端口上运行(详细信息将在一分钟内提供),并在可配置的路由上侦听GET请求
  • ResilienceSettings只是一个简单的helper类来存储超时和重试策略的配置值
  • 在服务器故障的情况下,我们定义了一个场景,它基本上是一个请求-响应对的序列
    • 为了测试重试策略,您可以指定中间步骤的数量
    • 在所有不成功的(中间)请求之后,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>派生类的一个实例。

最后,这是你如何初始化你的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 } }));
}

最新更新