使用Polly对HttpClient进行单元测试

4

我想对一个具有PollyRetryPolicyHttpClient进行单元测试,我正在尝试弄清楚如何控制HTTP响应。

我已经在客户端上使用了一个HttpMessageHandler,然后重写了SendAsync,这很好用,但是当我添加一个Polly Retry Policy时,我必须使用IServiceCollection创建HTTP客户端的实例,并且无法为客户端创建HttpMessageHandler。我尝试使用.AddHttpMessageHandler(),但这会阻止Polly Retry Policy,使其只触发一次。

以下是我在测试中设置HTTP客户端的方式:

IServiceCollection services = new ServiceCollection();

const string TestClient = "TestClient";
 
services.AddHttpClient(name: TestClient)
         .AddHttpMessageHandler()
         .SetHandlerLifetime(TimeSpan.FromMinutes(5))
         .AddPolicyHandler(KYA_GroupService.ProductMessage.ProductMessageHandler.GetRetryPolicy());

HttpClient configuredClient =
                services
                    .BuildServiceProvider()
                    .GetRequiredService<IHttpClientFactory>()
                    .CreateClient(TestClient);

public static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
    return HttpPolicyExtensions
            .HandleTransientHttpError()
            .WaitAndRetryAsync(6,
                retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
                onRetryAsync: OnRetryAsync);
}

private async static Task OnRetryAsync(DelegateResult<HttpResponseMessage> outcome, TimeSpan timespan, int retryCount, Context context)
{
    //Log result
}

当我调用_httpClient.SendAsync(httpRequestMessage)时,它会触发请求,但实际上会创建一个到地址的Http调用,我需要以某种方式拦截它并返回受控响应。

我想测试策略是否用于在请求失败时重试请求,并在获得完整响应后完成。

我主要的限制是我不能在MSTest中使用Moq。

2个回答

9

在单元测试中,您不希望您的HttpClient发出真正的HTTP请求 - 这将是一项集成测试。为了避免进行真实请求,您需要提供一个自定义的HttpMessageHandler。根据您在帖子中的规定,您不想使用模拟框架,因此,您可以提供一个存根而不是模拟HttpMessageHandler

受Polly GitHub页面上评论的强烈影响,我已经调整了您的示例,以调用一个存根化的HttpMessageHandler,该处理程序在第一次被调用时引发500错误,然后在随后的请求中返回200。

这个测试断言重试处理程序被调用,并且当执行步骤通过对HttpClient.SendAsync的调用时,生成的响应具有状态码为200:

public class HttpClient_Polly_Test
{
    const string TestClient = "TestClient";
    private bool _isRetryCalled;

    [Fact]
    public async Task Given_A_Retry_Policy_Has_Been_Registered_For_A_HttpClient_When_The_HttpRequest_Fails_Then_The_Request_Is_Retried()
    {
        // Arrange 
        IServiceCollection services = new ServiceCollection();
        _isRetryCalled = false;

        services.AddHttpClient(TestClient)
            .AddPolicyHandler(GetRetryPolicy())
            .AddHttpMessageHandler(() => new StubDelegatingHandler());

        HttpClient configuredClient =
            services
                .BuildServiceProvider()
                .GetRequiredService<IHttpClientFactory>()
                .CreateClient(TestClient);

        // Act
        var result = await configuredClient.GetAsync("https://www.stackoverflow.com");

        // Assert
        Assert.True(_isRetryCalled);
        Assert.Equal(HttpStatusCode.OK, result.StatusCode);
    }

    public IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
    {
        return HttpPolicyExtensions.HandleTransientHttpError()
            .WaitAndRetryAsync(
                6,
                retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
                onRetryAsync: OnRetryAsync);
    }

    private async Task OnRetryAsync(DelegateResult<HttpResponseMessage> outcome, TimeSpan timespan, int retryCount, Context context)
    {
        //Log result
        _isRetryCalled = true;
    }
}

public class StubDelegatingHandler : DelegatingHandler
{
    private int _count = 0;

    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        if (_count == 0)
        {
            _count++;
            return Task.FromResult(new HttpResponseMessage(HttpStatusCode.InternalServerError));
        }

        return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK));
    }
}

谢谢,我之前尝试过类似的方法但没有起作用。看来调用AddPolicyHandler和AddHttpMessageHandler的顺序很重要。先调用AddHttpMessageHandler会导致重试策略不起作用,将它们交换一下位置,让AddHttpMessageHandler成为最后一个被调用的方法,这样就能正常工作了。 - David Molyneux
这几乎是正确的,但通过添加“https://www.stackoverflow.com”,您也可以对该端点的连接进行单元测试,这是一个外部依赖项。如果您的单元测试在不允许在Intranet之外执行http请求的计算机上运行,会发生什么? - cozmin-calin

1

上面的答案对我有很大的帮助,让我找到了正确的方向。然而,我想测试是否已经将策略添加到了类型化的http客户端中。这个客户端是在应用程序启动时定义的。所以问题在于如何在类型化的客户端定义中指定的处理程序之后添加一个存根委托处理程序,并将其添加到服务集合中。

我能够利用IHttpMessageHandlerBuilderFilter.Configure并将我的存根处理程序添加为链中的最后一个处理程序。

public sealed class HttpClientInterceptionFilter : IHttpMessageHandlerBuilderFilter
{
    HandlerConfig handlerconfig { get; set; }

    public HttpClientInterceptionFilter(HandlerConfig calls)
    {
        handlerconfig = calls;
    }
    /// <inheritdoc/>
    public Action<HttpMessageHandlerBuilder> Configure(Action<HttpMessageHandlerBuilder> next)
    {
        return (builder) =>
        {
            // Run any actions the application has configured for itself
            next(builder);

            // Add the interceptor as the last message handler
            builder.AdditionalHandlers.Add(new StubDelegatingHandler(handlerconfig));
        };
    }
}

在你的单元测试中,将这个类注册到DI容器中:

services.AddTransient<IHttpMessageHandlerBuilderFilter>(n => new HttpClientInterceptionFilter(handlerConfig));

我需要把参数传递给存根处理程序,并从中获取数据并将其返回到我的单元测试中。我使用了这个类来完成:

public class HandlerConfig
{
    public int CallCount { get; set; }
    public DateTime[] CallTimes { get; set; }
    public int BackOffSeconds { get; set; }
    public ErrorTypeEnum ErrorType { get; set; }
}

public enum ErrorTypeEnum
{
    Transient,
    TooManyRequests
}

我的存根处理程序生成了短暂的和太多的请求响应:

public class StubDelegatingHandler : DelegatingHandler
{
    private HandlerConfig _config;
    HttpStatusCode[] TransientErrors = new HttpStatusCode[] { HttpStatusCode.RequestTimeout, HttpStatusCode.InternalServerError, HttpStatusCode.OK };

    public StubDelegatingHandler(HandlerConfig config)
    {
        _config = config;
    }
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        _config.CallTimes[_config.CallCount] = DateTime.Now;

        if (_config.ErrorType == ErrorTypeEnum.Transient)
        {              
            var response = new HttpResponseMessage(TransientErrors[_config.CallCount]);
            _config.CallCount++;
            return Task.FromResult(response);
        }

        HttpResponseMessage response429;
        if (_config.CallCount < 2)
        {
            //generate 429 errors
            response429 = new HttpResponseMessage(HttpStatusCode.TooManyRequests);
            response429.Headers.Date = DateTime.UtcNow;

            DateTimeOffset dateTimeOffSet = DateTimeOffset.UtcNow.Add(new TimeSpan(0, 0, 5));
            long resetDateTime = dateTimeOffSet.ToUnixTimeSeconds();
            response429.Headers.Add("x-rate-limit-reset", resetDateTime.ToString());
        }
        else
        {
            response429 = new HttpResponseMessage(HttpStatusCode.OK);
        }

        _config.CallCount++;

        return Task.FromResult(response429);

    }
}

最后是单元测试:

[TestMethod]
public async Task Given_A_429_Retry_Policy_Has_Been_Registered_For_A_HttpClient_When_429_Errors_Occur_Then_The_Request_Is_Retried()
    {
        // Arrange 
        IServiceCollection services = new ServiceCollection();

        var handlerConfig = new HandlerConfig { ErrorType = ErrorTypeEnum.TooManyRequests, BackOffSeconds = 5, CallTimes = new System.DateTime[RetryCount] };

        // this registers a stub message handler that returns the desired error codes
        services.AddTransient<IHttpMessageHandlerBuilderFilter>(n => new HttpClientInterceptionFilter(handlerConfig));

        services.ConfigureAPIClient();  //this is an extension method that adds a typed client to the services collection

        HttpClient configuredClient =
            services
                .BuildServiceProvider()
                .GetRequiredService<IHttpClientFactory>()
               .CreateClient("APIClient");  //Note this must be the same name used in ConfigureAPIClient

        //  Act
        var result = await configuredClient.GetAsync("https://localhost/test");

        //   Assert
        Assert.AreEqual(3, handlerConfig.CallCount, "Expected number of  calls made");
        Assert.AreEqual(HttpStatusCode.OK, result.StatusCode, "Verfiy status code");

        var actualWaitTime = handlerConfig.CallTimes[1] - handlerConfig.CallTimes[0];
        var expectedWaitTime = handlerConfig.BackOffSeconds + 1;  //ConfigureAPIClient adds one second to give a little buffer
        Assert.AreEqual(expectedWaitTime, actualWaitTime.Seconds);           
    }
}

您似乎正在使用命名的而非类型化的HTTP客户端。 - more urgent jest

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接