Search code examples
c#unit-testingdotnet-httpclientpolly

How can I test a Polly retry policy that's set during startup?


During startup I basically add an HttpClient like this:

services.AddHttpClient<IHttpClient, MyHttpClient>().AddPolicyHandler(GetRetryPolicy());

public IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
    HttpPolicyExtensions
        .HandleTransientHttpError()
        .OrResult(message => message.StatusCode == HttpStatusCode.NotFound)
        .WaitAndRetryAsync(GetBackOffDelay(options),
            onRetry: (result, timespan, retryAttempt, context) =>
            {
                context.GetLogger()?.LogWarning($"Failure with status code {result.Result.StatusCode}. Retry attempt {retryAttempt}. Retrying in {timespan}.");
            }));
}

How can I test that the retry policy works as expected? I've tried writing a test like this:

public async Task MyTest()
{
    var policy = GetMockRetryPolicy(); // returns the policy shown above.
    var content = HttpStatusCode.InternalServerError;
    var services = new ServiceCollection();
    services.AddHttpClient<IHttpClient, MyFakeClient>()
        .AddPolicyHandler(policy);

    var client = (MyFakeClient)services.BuildServiceProvider().GetRequiredService<IHttpClient>();
    await client.Post(new Uri("https://someurl.com")), content);

    // Some asserts that don't work right now
}

For reference here's the bulk of my Post method on MyFakeClient:

if(Enum.TryParse(content.ToString(), out HttpStatusCode statusCode))
{
    if(statusCode == HttpStatusCode.InternalServerError)
    {
        throw new HttpResponseException(new Exception("Internal Server Error"), (int)HttpStatusCode.InternalServerError);
    }
}

The MyFakeClient has a Post method that checks to see if the content is an HttpStatusCode and if it's an internal server error throws an HttpResponseException. At the moment, this creates the correct client and triggers the post fine. It throws the HttpResponseException but in doing so exits the test rather than using the retry policy.

How can I get it to use the retry policy in the test?

Update

I followed Peter's advice and went down the integration test route and managed to get this to work using hostbuilder and a stub delegating handler. In the stub handler I pass in a custom header to enable me to read the retry count back out of the response. The retry count is just a property on the handler that gets incremented every time it's called, so it's actually the first attempt, plus all following retries. That means if the retry count is 3, you should expect 4 as the value.


Solution

  • The thing is you can't really unit test this:

    1. The retry is registered on the top of the HttpClient via the DI. When you are unit testing then you are not relying on the DI rather on individual components. So, integration testing might be more suitable for this. I've already detailed how can you do that via WireMock.Net. The basic idea is to create a local http server (mocking the downstream system) with predefined response sequence.
    2. After you have defined the retry policy (with the retry count, time penalties) you can not retrieve them easily. So, from a unit testing perspective it is really hard to make sure that the policy has been defined correctly (like the delay is specified in seconds, not in minutes). I've already created a github issue for that, but unfortunately the development of the new version has been stuck.