Testing HttpClient

Due to its peculiar architecture, testing components depending on HttpClient is quite complicated.

To understand the challenges, it is important to keep in mind that HttpClient is not a service that can be mocked (in fact, it does not implement any interface), rather a facade around a pipeline of instances of HttpMessageHandler subclasses. Here is a series of posts by Steve Gordon that touch the complexity of the HttpClient and related components.

Mocking HttpClient in reality equates to creating an instance of HttpClient with a fake HttpMessageHandler. Unfortunately, this is easier said than done as there are many aspects to be taken into consideration when creating a fake HttpMessageHandler.

The library MockHttp exposes a subclass of the HttpMessageHandler abstract class specifically designed to facilitate unit testing, providing a fluent configuration API. Since the system under test is consuming the HttpClient, it will remain unaware that the MockHttpMessageHandler is being used.

Let's consider this service using HttpClient as test subject

public class Service
{
    private readonly HttpClient _http;

    public Service(HttpClient http)
    {
        _http = http ?? throw new ArgumentNullException(nameof(http));
    }

    public Task<string> GetStringAsync(Uri uri)
    {
        return _http.GetStringAsync(uri);
    }
}

We can write a test that asserts that GetStringAsync retrieves the content from a certain URI and returns it as string. By using MockHttp, we can arrange the handler so that when a GET request is issued to the given URI, a given content is returned.

[Test]
public async Task GetStringAsync_uses_HttpClient_to_get_content_from_given_URI()
{
    // ARRANGE
    var fixture = new Fixture();
    var testUri = fixture.Create<Uri>();
    var expectedResult = fixture.Create<string>();

    var handler = new MockHttpMessageHandler();
    handler.When(HttpMethod.Get, testUri.ToString())
           .Respond(HttpStatusCode.OK, new StringContent(expectedResult));

    var http = handler.ToHttpClient();

    var sut = new Service(http);

    // ACT
    var result = await sut.GetStringAsync(testUri);

    // ASSERT
    Assert.That(result, Is.EqualTo(expectedResult));
}

The same test can be written in a much more concise form by leveraging a glue-library for AutoFixture that uses MockHttp. This library can be obtained from NuGet under the name of Kralizek.AutoFixture.Extensions.MockHttp. The library is built around a specimen builder that, when asked for an HttpClient, creates one that uses a MockHttpMessageHandler internally. Since the MockHttpMessageHandler is resolved using AutoFixture, we can leverage patterns like freezing and injection to retain access to the internal instance.

[Test]
public async Task GetStringAsync_uses_HttpClient_to_get_content_from_given_URI()
{
    // ARRANGE
    var fixture = new Fixture().AddMockHttp();

    var testUri = fixture.Create<Uri>();
    var expectedResult = fixture.Create<string>();

    var handler = fixture.Freeze<MockHttpMessageHandler>();
    handler.When(HttpMethod.Get, testUri.ToString())
           .Respond(HttpStatusCode.OK, new StringContent(expectedResult));

    var sut = fixture.Create<Service>();

    // ACT
    var result = await sut.GetStringAsync(testUri);

    // ASSERT
    Assert.That(result, Is.EqualTo(expectedResult));
}

Things get really interesting when we combine MockHttp with a AutoData attribute

[AttributeUsage(AttributeTargets.Method)]
public class HttpAutoDataAttribute : AutoDataAttribute
{
    public HttpAutoDataAttribute() : base (CreateFixture) {}

    private IFixture CreateFixture()
    {
        var fixture = new Fixture();

        fixture.AddMockHttp();

        return fixture;
    }
}

With the attribute defined, we can rewrite the unit test like this:

[Test, HttpAutoData]
public async Task GetStringAsync_uses_HttpClient_to_get_content_from_given_URI([Frozen] MockHttpMessageHandler handler, Service sut, Uri testUri, string expectedResult)
{
    // ARRANGE
    handler.When(HttpMethod.Get, testUri.ToString())
           .Respond(HttpStatusCode.OK, new StringContent(expectedResult));

    // ACT
    var result = await sut.GetStringAsync(testUri);

    // ASSERT
    Assert.That(result, Is.EqualTo(expectedResult));
}

As shown in the snippet above, by leveraging the integration between AutoFixture, NUnit and MockHttp, it is possible to write very concise yet powerful tests for components using the HttpClient class and its related components.

HttpClientFactory

To face some of the issues caused by bad usage of HttpClient (like socket exhaustion and DNS cache pinning), Microsoft included in .NET 2.1 a new API often referred to as HttpClientFactory.

Developers can leverage the new API in two ways:

  • by instructing the dependency injection engine to use the HttpClientFactory when requested an instance of HttpClient

  • by replacing the dependency to be a IHttpClientFactory and use it to fetch an instance of HttpClient

Since the first approach deosn't alter the test subject, the same setup as shown above can be used.

On the other hand, consuming an IHttpClientFactory requires some changes to the unit tests.

Here is the test subject modified so that it consumes an IHttpClientFactory.

public class Service
{
    private readonly IHttpClientFactory _httpFactory;

    public Service(IHttpClientFactory httpFactory)
    {
        _httpFactory = httpFactory ?? throw new ArgumentNullException(nameof(httpFactory));
    }

    public Task<string> GetStringAsync(Uri uri)
    {
        var http = _httpFactory.CreateClient(nameof(Service));

        return http.GetStringAsync(uri);
    }
}

Since we're dealing with an interface, we can use Moq to build a test fake.

public async Task GetStringAsync_uses_HttpClient_to_get_content_from_given_URI()
{
    // ARRANGE
    var fixture = new Fixture();
    var testUri = fixture.Create<Uri>();
    var expectedResult = fixture.Create<string>();

    var handler = new MockHttpMessageHandler();
    handler.When(HttpMethod.Get, testUri.ToString())
           .Respond(HttpStatusCode.OK, new StringContent(expectedResult));

    var http = handler.ToHttpClient();

    var mockHttpClientFactory = new Mock<IHttpClientFactory>();
    mockHttpClientFactory.Setup(p => p.CreateClient(It.IsAny<string>())).Returns(http);

    var sut = new Service(mockHttpClientFactory.Object);

    // ACT
    var result = await sut.GetStringAsync(testUri);

    // ASSERT
    Assert.That(result, Is.EqualTo(expectedResult));
}

Quite interestingly, when converting the unit test above into one leveraging the AutoData attribute, we get the exact same unit test as our example with a HttpClient. The complexity of IHttpClientFactory.CreateClient is handled automatically by Moq, once AutoMoq is configured accordingly.

[AttributeUsage(AttributeTargets.Method)]
public class HttpAutoDataAttribute : AutoDataAttribute
{
    public HttpAutoDataAttribute() : base (CreateFixture) {}

    private IFixture CreateFixture()
    {
        var fixture = new Fixture();

        fixture.AddMockHttp();

        fixture.Customize(new AutoMoqCustomization { ConfigureMembers = true, GenerateDelegates = true });

        return fixture;
    }
}

[Test, HttpAutoData]
public async Task GetStringAsync_uses_HttpClient_to_get_content_from_given_URI([Frozen] MockHttpMessageHandler handler, Service sut, Uri testUri, string expectedResult)
{
    // ARRANGE
    handler.When(HttpMethod.Get, testUri.ToString())
           .Respond(HttpStatusCode.OK, new StringContent(expectedResult));

    // ACT
    var result = await sut.GetStringAsync(testUri);

    // ASSERT
    Assert.That(result, Is.EqualTo(expectedResult));
}

Last updated