Unit Test Typed HttpClient with Microsoft.Extensions.Http.Resilience

4 min read 04-10-2024
Unit Test Typed HttpClient with Microsoft.Extensions.Http.Resilience


Unit Testing Typed HttpClient with Microsoft.Extensions.Http.Resilience: A Comprehensive Guide

Problem: Unit testing a typed HttpClient that uses Microsoft.Extensions.Http.Resilience can be tricky. How do you effectively test the resilience policies and their interactions with your actual API calls?

Rephrased: Imagine you're building a robust application that makes API calls, but you want to ensure those calls are resilient in case of errors or network issues. You're using a Typed HttpClient with features like retries and timeouts provided by Microsoft.Extensions.Http.Resilience. Now you need to write tests to confirm your resilience policies are working as intended.

Scenario:

Let's say you have a simple WeatherForecastService that makes a request to a weather API using a Typed HttpClient:

public class WeatherForecastService
{
    private readonly IHttpClientFactory _httpClientFactory;

    public WeatherForecastService(IHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory;
    }

    public async Task<WeatherForecast> GetForecastAsync(string city)
    {
        var client = _httpClientFactory.CreateClient("WeatherApi");
        var response = await client.GetAsync({{content}}quot;api/weather/{city}");
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadFromJsonAsync<WeatherForecast>();
    }
}

This service uses IHttpClientFactory to obtain a preconfigured HttpClient named "WeatherApi" that includes resilience policies. The "WeatherApi" client is configured in Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    services.AddHttpClient("WeatherApi", client =>
    {
        client.BaseAddress = new Uri("https://api.example.com");
    })
    .AddTransientHttpErrorPolicy(builder => builder.WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))))
    .AddTimeoutPolicy(TimeSpan.FromSeconds(5));
}

Challenge:

How do we unit test this service and verify that the resilience policies are working as intended? We can't simply mock HttpClient because the resilience policies are applied at the IHttpClientFactory level, not directly on the HttpClient instance.

Solution:

We need to create a test environment that allows us to control the behavior of the API and observe the resilience policy's actions. Here's how we can approach it:

  1. Mocking IHttpClientFactory: We can mock IHttpClientFactory to create a HttpClient that allows us to control the response to the API call.
  2. Using FakeHttp: We can leverage a library like FakeHttp to provide a mocked HTTP server that will respond to our requests in a controlled manner.
  3. Asserting on Resilience Policy Logic: We can assert that the retry logic and timeout logic within the WaitAndRetryAsync and AddTimeoutPolicy are being triggered correctly.

Example Test Code:

using FakeHttp;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http;
using Xunit;

public class WeatherForecastServiceTest
{
    private readonly IServiceProvider _serviceProvider;

    public WeatherForecastServiceTest()
    {
        var services = new ServiceCollection();
        services.AddHttpClient("WeatherApi", client => 
        {
            client.BaseAddress = new Uri("https://api.example.com");
        })
        .AddTransientHttpErrorPolicy(builder => builder.WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))))
        .AddTimeoutPolicy(TimeSpan.FromSeconds(5));
        services.AddSingleton<IHttpClientFactory>(sp => 
        {
            var fakeHttp = new FakeHttpServer();
            fakeHttp.AddRule(HttpMethod.Get, "/api/weather/london", (req, res) =>
            {
                // Simulate an error on the first attempt
                if (fakeHttp.RequestCount == 1)
                {
                    res.SetStatusCode(HttpStatusCode.InternalServerError);
                    return;
                }

                res.SetStatusCode(HttpStatusCode.OK);
                res.WriteContent(@"{""temperature"": 20}");
            });
            return new DelegatingHttpClientFactory(fakeHttp, "WeatherApi");
        });
        _serviceProvider = services.BuildServiceProvider();
    }

    [Fact]
    public async Task GetForecastAsync_ShouldRetryOnFailureAndSucceed()
    {
        var service = _serviceProvider.GetRequiredService<WeatherForecastService>();
        var forecast = await service.GetForecastAsync("london");

        Assert.Equal(20, forecast.Temperature);
        // Verify that the API call was made 3 times (initial attempt + 2 retries)
        Assert.Equal(3, _serviceProvider.GetRequiredService<FakeHttpServer>().RequestCount);
    }
}

Key Insights:

  • Controllable Environment: By using FakeHttp, we create a controlled environment where we can simulate different API responses and observe the behavior of the resilience policies.
  • Assert on Resilience Logic: We can assert not only the final result of the API call but also how many times the API was called (in the case of retries) and whether a timeout was triggered.
  • Flexibility: The FakeHttp library allows you to simulate various scenarios, including network errors, slow responses, and different HTTP status codes, enabling you to test different aspects of your resilience policies.

Conclusion:

Unit testing your Typed HttpClient with Microsoft.Extensions.Http.Resilience effectively requires a strategy that goes beyond simple mocking. By utilizing libraries like FakeHttp and strategically mocking the IHttpClientFactory, you can build robust tests that ensure your application is resilient and handles errors gracefully.

Additional Resources: