Search code examples
c#unit-testingdotnet-httpclientpollyretry-logic

Unit test HttpClient with Polly


I'm looking to unit test a HttpClient that has a Polly RetryPolicy and I'm trying to work out how to control what the HTTP response will be.

I have used a HttpMessageHandler on the client and then override the Send Async and this works great but when I add a Polly Retry Policy I have to create an instance of HTTP Client using the IServiceCollection and cant create a HttpMessageHandler for the client. I have tried using the .AddHttpMessageHandler() but this then blocks the Poll Retry Policy and it only fires off once.

This is how I set up my HTTP client in my test

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
}

This will then fire the request when I call _httpClient.SendAsync(httpRequestMessage) but it actualy create a Http call to address and I need to intercept this some how and return a controlled response.

I would like to test that the policy is used to retry the request if the request fails and completes when it is a complete response.

The main restriction I have is I can't use Moq on MSTest.


Solution

  • You don't want your HttpClient to be issuing real HTTP requests as part of a unit test - that would be an integration test. To avoid making real requests you need to provide a custom HttpMessageHandler. You've stipulated in your post that you don't want to use a mocking framework, so rather than mocking HttpMessageHandler you could provide a stub.

    With heavy influence from this comment on an issue on Polly's GitHub page, I've adjusted your example to call a stubbed HttpMessageHandler which throws a 500 the first time it's called, and then returns a 200 on subsequent requests.

    The test asserts that the retry handler is called, and that when execution steps past the call to HttpClient.SendAsync the resulting response has a status code of 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));
        }
    }