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:
- Mocking
IHttpClientFactory
: We can mockIHttpClientFactory
to create aHttpClient
that allows us to control the response to the API call. - Using
FakeHttp
: We can leverage a library likeFakeHttp
to provide a mocked HTTP server that will respond to our requests in a controlled manner. - Asserting on Resilience Policy Logic: We can assert that the retry logic and timeout logic within the
WaitAndRetryAsync
andAddTimeoutPolicy
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:
- FakeHttp: https://github.com/nicholasbishop/FakeHttp
- Microsoft.Extensions.Http.Resilience: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-7.0
- HttpClientFactory: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-7.0