Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 927
David Molyneux Avatar asked Nov 07 '25 09:11

David Molyneux


1 Answers

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));
    }
}
like image 172
simon-pearson Avatar answered Nov 10 '25 01:11

simon-pearson



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!