繁体   English   中英

使用 Polly 对 HttpClient 进行单元测试

[英]Unit test HttpClient with Polly

我希望对具有Polly RetryPolicyHttpClient进行单元测试,并且我正在尝试弄清楚如何控制HTTP响应将是什么。

我在客户端上使用了HttpMessageHandler ,然后覆盖了 Send Async,这很好用,但是当我添加 Polly 重试策略时,我必须使用IServiceCollection创建 HTTP 客户端的实例,并且无法为客户端创建HttpMessageHandler 我曾尝试使用.AddHttpMessageHandler()但这会阻止轮询重试策略并且它只会触发一次。

这就是我在测试中设置 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。

您不希望您的HttpClient作为单元测试的一部分发出真正的 HTTP 请求 - 这将是一个集成测试。 为避免发出真正的请求,您需要提供自定义HttpMessageHandler 您在帖子中规定您不想使用 mocking 框架,因此您可以提供一个stub ,而不是 mocking 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));
    }
}

上面的答案对让我走上正轨非常有帮助。 但是我想测试策略是否已添加到类型化的 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));
        };
    }
}

将此 class 注册到单元测试中的 DI 容器中:

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

我需要将一些参数传递给存根处理程序并从中获取数据并返回到我的单元测试。 我用这个 class 这样做:

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM