【问题标题】:Unit test HttpClient with Polly使用 Polly 对 HttpClient 进行单元测试
【发布时间】:2020-04-16 15:57:12
【问题描述】:

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

我在客户端上使用了HttpMessageHandler,然后覆盖了 Send Async,这很好用,但是当我添加 Polly 重试策略时,我必须使用 IServiceCollection 创建 HTTP 客户端实例并且无法创建 @ 987654327@为客户。我曾尝试使用.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。

【问题讨论】:

    标签: c# unit-testing httpclient polly


    【解决方案1】:

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

    this 对 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 是最后一次调用,它按预期工作
    • 这几乎没问题,但是通过添加“stackoverflow.com”,您还可以对与该端点的连接进行单元测试,这是一个外部依赖项。如果您的单元测试在不允许在内网外部进行 http 请求的机器上运行会发生什么?
    【解决方案2】:

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

    【讨论】:

      猜你喜欢
      • 2017-03-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-07-25
      • 2013-02-25
      • 2018-05-21
      • 1970-01-01
      相关资源
      最近更新 更多