What is a typed HTTP client? In the simplest words, a strongly typed HTTP client is a class that has HttpClient dependency. Why do I prefer this solution over named HTTP clients?

  • You can easily inject the necessary dependencies into them.
  • There is no need to use strings as keys.
  • You may use IntelliSense while consuming them.

Here is an example of a client for which we will try to implement tests:

public class MyAwesomeHttpClient
{
    private readonly HttpClient _httpClient;
    private readonly ISomeOtherDependency _someOtherDependency;

    public MyAwesomeHttpClient(
        HttpClient httpClient,
        ISomeOtherDependency someOtherDependency)
    {
        _httpClient = httpClient;
        _someOtherDependency = someOtherDependency ?? throw new ArgumentNullException(nameof(someOtherDependency));
    }

    public async Task<string> GetRandomDadJoke()
    {
        var request = new HttpRequestMessage(HttpMethod.Get, new Uri("/", UriKind.Relative));
        var response = await _httpClient.SendAsync(request);

        if (!response.IsSuccessStatusCode)
            throw new Exception("I can't haz dad joke...");

        _someOtherDependency.DoNothig();
        var joke = await response.Content.ReadAsStringAsync();

        return joke;
    }
}

How to register and configure typed HTTP client?

To use typed HTTP clients, you must first register them in dependency injection container. Personally, I prefer to create a dedicated extension method, which I will then call on the service collection. Such an extension method has the advantage that I can register all the client’s dependencies and configure it in one place. Here is an example implementation:

public static IServiceCollection AddMyAwesomeHttpClient(this IServiceCollection services)
{
    services
        // Let's register our HTTP client
        .AddHttpClient<MyAwesomeHttpClient>()
        // Then we need to configure it
        .ConfigureHttpClient(httpClient =>
        {
            httpClient.BaseAddress = new Uri("https://icanhazdadjoke.com", UriKind.Absolute);
            httpClient.DefaultRequestHeaders
                .Add(nameof(HttpRequestHeader.Accept), MediaTypeNames.Text.Plain);
        })
        // Or maybe we want to add some exception policies to it?
        .AddPolicyHandler(HttpPolicyExtensions
            .HandleTransientHttpError()
            .RetryAsync(retryCount: 3));
    
    // Don't not forget about our client's dependencies!
    services.TryAddTransient<ISomeOtherDependency, SomeOtherDependency>();

    return services;
}

Let’s try to implement the tests.

First we need to mock the HttpClient

The core problem is mocking HttpClient, or rather HttpMessageHandler, which is used by HttpClient injected to the typed HTTP client.

For creating HttpMessageHandler mocks, I highly recommend the great MockHttp library created by Richard Szalay. Take a look at this example:

var httpMessageHandlerMock = new MockHttpMessageHandler();
// Mock the request
var mockRequest = httpMessageHandlerMock
        .When("https://icanhazdadjoke.com/")
        .Respond(
            MediaTypeNames.Text.Plain,
            "Why can't your nose be 12 inches long? Because then it'd be a foot!");
// Create a mock HttpClient
var httpClient = httpMessageHandlerMock.ToHttpClient();

var response = await httpClient.GetStringAsync("https://icanhazdadjoke.com/");
// response: Why can't your nose be 12 inches long? Because then it'd be a foot!

I think this tool is very useful.

The most obvious method of testing

Take a look at this sample test:

[Test]
public async Task MyAwesomeHttpClient_ShouldSucceed_WhenRequestingForRandomDadJoke()
{
    const string ValidJokeResponse =
        "My dog used to chase people on a bike a lot. It got so bad I had to take his bike away.";
    // Create and configure HttpMessageHandler mock
    var httpMessageHandlerMock = new MockHttpMessageHandler();
    httpMessageHandlerMock
        .When(HttpMethod.Get, "https://icanhazdadjoke.com/")
        .Respond(MediaTypeNames.Text.Plain, ValidJokeResponse);
    // Create mock HttpClient out of HttpMessageHandler mock
    var httpClientMock = httpMessageHandlerMock.ToHttpClient();
    // Configure it manually
    httpClientMock.BaseAddress = new Uri("https://icanhazdadjoke.com/", UriKind.Absolute);
    // Create MyAwesomeHttpClient instance manually
    var sut = new MyAwesomeHttpClient(
        httpClientMock,
        Mock.Of<ISomeOtherDependency>());

    var result = await sut.GetRandomDadJoke();

    result.Should()
        .Be(ValidJokeResponse);
}

I’m not happy with it. Actually, we inject not the same HttpClient that we configured with our AddMyAwesomeHttpClient extension method. At first glance, we can see several problems:

  • We need to manually inject dependencies into the typed client, instead of using a DI container for that.
  • The HttpClient we are mocking is not configured by the ConfigureHttpClient method. We need to configure it redundantly for the test purposes.
  • Mocked HttpClient is missing all exception policies we have configured.

It would definitely be best to test the HTTP client that we registered in the DI container with the AddMyAwesomeHttpClient method. Why? Because it is already properly configured and at the same time, we can test the AddMyAwesomeHttpClient method.

So, how to test it better?

The main idea is to create a test DI container, register our client in it with AddMyAwesomeHttpClient extension method, and then replace its primary HttpMessageHandler with mock.

In order to achieve this, we need to create a helper class, which implements IHttpMessageHandlerBuilderFilter interface. Its task will be to replace the primary HttpMessageHandler of the tested HttpClient with mock. Let’s say we name this class TestHttpMessageHandlerBuilderFilter.

The second thing is that we want to be able to overwrite the handlers of multiple clients registered in the DI container at the same time. For this reason, we also need to implement some kind of the HttpMessageHandler mock registry that our TestHttpMessageHandlerBuilderFilter will use. To simplify this issue, we can use our test DI container as a registry for the following entries:

internal sealed class HttpMessageHandlerMockWrapper
{
    public HttpMessageHandlerMockWrapper(
        Type typedHttpClientType,
        HttpMessageHandler httpMessageHandlerMock)
    {
        TypedHttpClientType = typedHttpClientType;
        HttpMessageHandlerMock = httpMessageHandlerMock;
    }
    
    public Type TypedHttpClientType { get; }
    public HttpMessageHandler HttpMessageHandlerMock { get; }
}

Our implementation of IHttpMessageHandlerBuilderFilter will look like this:

internal sealed class TestHttpMessageHandlerBuilderFilter
    : IHttpMessageHandlerBuilderFilter
{
    private readonly IEnumerable<HttpMessageHandlerMockWrapper> _httpMessageHandlerWrappers;

    public TestHttpMessageHandlerBuilderFilter(
        // Injection of previously registered maps
        IEnumerable<HttpMessageHandlerMockWrapper> httpMessageHandlerWrappers)

    {
        _httpMessageHandlerWrappers = httpMessageHandlerWrappers;
    }

    public Action<HttpMessageHandlerBuilder> Configure(Action<HttpMessageHandlerBuilder> next)
    {
        return builder =>
        {
            // Checking if a given HttpClient has a registered HttpMessageHandler mock
            var mockHandlerWrapper = _httpMessageHandlerWrappers
                .SingleOrDefault(x =>
                    x.TypedHttpClientType.Name.Equals(
                        builder.Name,
                        StringComparison.InvariantCultureIgnoreCase));

            if (mockHandlerWrapper is not null)
            {
                // If so, the default handler is replaced with mock
                Debug.WriteLine($"Overriding {nameof(builder.PrimaryHandler)} for '{builder.Name}' typed HTTP client");
                builder.PrimaryHandler = mockHandlerWrapper.HttpMessageHandlerMock;
            }
            next(builder);
        };
    }
}

Now we have to register everything in the test DI container. Let’s create an appropriate extension method for this purpose.

internal static class DependencyInjectionExtensions
{
    public static IServiceCollection OverridePrimaryHttpMessageHandler<TClient>(
        this IServiceCollection services,
        HttpMessageHandler mockMessageHandler)
    {
        if (services == null) throw new ArgumentNullException(nameof(services));
        if (mockMessageHandler == null) throw new ArgumentNullException(nameof(mockMessageHandler));

        // Register a mock handler
        services
            .AddTransient(_ => new HttpMessageHandlerMockWrapper(typeof(TClient), mockMessageHandler));

        // Replace the default or already registered IHttpMessageHandlerBuilderFilter
        // with our TestHttpMessageHandlerBuilderFilter
        services
            .Replace(
                ServiceDescriptor
                    .Transient<IHttpMessageHandlerBuilderFilter, TestHttpMessageHandlerBuilderFilter>());

        return services;
    }
}

Putting it all together

Let’s try to write a test of our typed client that will use dependency injection and our mocking mechanism.

[Test]
public async Task MyAwesomeTypedHttpClient_ShouldSucceed_WhenRequestingForRandomDadJoke()
{
    const string ValidJokeResponse =
        "My dog used to chase people on a bike a lot. It got so bad I had to take his bike away.";
    // Create and configure our HttpMessageHandler mock
    var httpMessageHandlerMock = new MockHttpMessageHandler();
    httpMessageHandlerMock
        .When(HttpMethod.Get, "https://icanhazdadjoke.com/")
        .Respond(MediaTypeNames.Text.Plain, ValidJokeResponse);
    // Create our test DI container
    var serviceProvider = new ServiceCollection()
        // Mock ISomeOtherDependency
        .AddTransient(_ => Mock.Of<ISomeOtherDependency>())
        // Add and configure our typed HTTP client
        .AddMyAwesomeHttpClient()
        // Overwrite what you need
        .OverridePrimaryHttpMessageHandler<MyAwesomeHttpClient>(httpMessageHandlerMock)
        .BuildServiceProvider();
    var sut = serviceProvider.GetRequiredService<MyAwesomeHttpClient>();

    var result = await sut.GetRandomDadJoke();

    result.Should()
        .Be(ValidJokeResponse);
}

It looks good and most importantly it works! We can even test our exception policy since we have fully configured HttpClient.

[TestCase(HttpStatusCode.BadRequest, 1)]
[TestCase(HttpStatusCode.InternalServerError, 4)]
public async Task MyAwesomeTypedHttpClient_ShouldFail_WhenReceiveErrorResponseCode(
    HttpStatusCode httpStatusCode, int failedRequestCountBeforeException)
{
    var httpMessageHandlerMock = new MockHttpMessageHandler();
    var testHttpRequestDefinition = httpMessageHandlerMock
        .When(HttpMethod.Get, "https://icanhazdadjoke.com/")
        .Respond(httpStatusCode);
    var serviceProvider = new ServiceCollection()
        .AddTransient(_ => Mock.Of<ISomeOtherDependency>())
        .AddMyAwesomeHttpClient()
        .OverridePrimaryHttpMessageHandler<MyAwesomeHttpClient>(httpMessageHandlerMock)
        .BuildServiceProvider();
    var sut = serviceProvider.GetRequiredService<MyAwesomeHttpClient>();

    var action = async () => await sut.GetRandomDadJoke();

    await action.Should()
        .ThrowAsync<Exception>();
    httpMessageHandlerMock.GetMatchCount(testHttpRequestDefinition)
        .Should().Be(failedRequestCountBeforeException);
}

Summary

Do you know a better way to test typed HTTP clients? Or maybe you have any questions or suggestions? If so, please don’t hesitate to contact me. I am curious what you think.

I created a sample application that I published on GitHub. Enjoy!