Using FakeHttpMessageHandler

Introduction

HTTP calls are one of the most popular ways to do client-to-service or service-to-service communication these days. Most C# developers use HttpClient type to make http calls. Using HttpClient correctly, requires a good understanding on its internals, but for testing, this knowledge is unavoidable.

In this post, I will look into how the Goldlight.HttpClientTestSupport library can be used to help writing unit tests against code with HttpClient.

Why Testing?

Properties of a good unit tests includes being fast, repeatable and isolated. Code that uses HttpClient typically depends on an external service that is being invoked. This invocation makes the unit tests to break all of these properties.

  1. Invoking an external service is never fast. A request needs to be serialized, sent over the network, where another service deserializes it, calculates the response which is sent back. This take several magnitudes longer compared to a method call, inherently making tests run significantly longer compared to tests without external service dependencies. In a worst case scenario the request might be timing out, which can increase the duration from a few seconds to minutes.

  2. The outcome of a test with external service dependency will not be consistent. When the external service is invoked, it might be down (due to an update rolling out, or hardware issues) or might just return an incorrect result (due to a bug or error in the service). Typically dev/qa/test environments hosting these services are not providing the same level of quality and availability as production environment. Creating a test against such a dependency will make the test brittle. Some test runs might complete successfully, and some might just fail.

  3. The key problem with such unit tests is that they are not isolated. They depend on another system, and they depend on the state of the other system. This state might change between test runs or even during the execution of the test, by other concurrent tests. We cannot test our unit of work independently from the rest of the system.

Note, I am strictly considering unit tests in this post. Integration tests will cover individual units working together in a system. Such tests are less likely to mock a service dependency.

The suggestion today is to create HttpClient instances through IHttpClientFactory. Injecting this dependency into our unit under test might make you believe that the problem is solved. However, the IHttpClientFactory's CreateClient() method still returns an HttpClient, so we cannot avoid returning an HttpClient typed object when preparing the mock. Using the Typed Client approach, even this first step is less evident. A developer needs to provide an actual HttpClient dependency to the unit under test. This make the business layer testable, but our typed client remains untested.

Testing the edge of the system, or testing impure methods is a more challenging exercise than testing pure methods. I already know that impureim-sandwich is a good architecture design for such systems, but it does not liberate us from writing tests against of impure layer.

The goal is to test the edge of our system. This might be critical code for the functionality of the application, hence we want to make sure to cover it with tests. The HTTP calls might need to set certain headers or send the content serialized to a given message and encoded into representation (such as XML or JSON). Handling the response message is just as important. Typed clients will check the status code of the response message, read the content of it and deserialize to POCO object, a string or to a stream of bytes. Optionally the response headers may alter the behavior of processing a response. Based on the response code a typed client can decide to log an error/warning, return empty (or null) result or throw an exception.

Testing HttpClient

The introduction should encourage the reader to write unit tests against code making HTTP calls. Mocking HttpClient is not trivial by just inspecting its implementation. The suggested approach is to create a custom message handler. This suggestion made me in the past to drop custom message handler implementations as private classes around my test codebase. Sometimes a single implementation is shared along multiple projects in the solution, but never across multiple solutions. I have never implemented a custom message handler in its entirety with all the hooks required for testing - only what I needed for the given test, following the YAGNI principal. However, I have recently come across the Goldlight.HttpClientTestSupport library, which provides a feature rich implementation of a fake http message hander. Using it is fairly simple:

  • add the nuget package: dotnet add package Goldlight.HttpClientTestSupport

  • after adding the required usings, create a new FakeHttpMessageHandler object

  • customize its behavior with fluent syntax

  • pass it to HttpClient's constructor

  • write the rest of the test case

I will create a test against the NugetClient class below. This type is written using the typed client approach.

public class NugetClient
{
    private readonly HttpClient _client;
    public NugetClient(HttpClient client) 
    {
         _client = client; 
    }

    public async Task<NugetResponse> GetDataAsync(string package)
    {
        var request = new HttpRequestMessage(HttpMethod.Get, $"https://api.nuget.org/v3-flatcontainer/{package}/index.json");
        request.Headers.TryAddWithoutValidation("Content-Type", "application/json");
        var response = await _client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
        response.EnsureSuccessStatusCode();
        using var contentStream = await response.Content.ReadAsStreamAsync();
        var nugetResponse = await JsonSerializer.DeserializeAsync<NugetResponse>(contentStream, new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });
        return nugetResponse;
    }
}

It depends on an HttpClient instance. The GetDataAsync() method sends an HTTP request to the nuget api for getting the available version of a package. The method makes sure to receive a successful HTTP code in the response, and if so, it deserializes the response content into a NugetResponse typed object.

Although it is not a production grade code, it serves the purpose of this post: to write a test against it.

Let's create the simplest test, testing a happy path. Note, that covering all test cases falls out of scope of this post. For this test, I am using xUnit.net, and I parameterize the test with an input parameter named version. This parameter will be used in the fake response message to the HTTP request.

[Theory]
[InlineData("1.1.0")]
public async Task WithSingleVersion_GetDataAsync_ReturnsDataWithVersion(string version)
{
    var messageHandler = new FakeHttpMessageHandler()
        .WithExpectedContent(new NugetResponse() { Versions = new List<string>() { version } })
        .WithStatusCode(System.Net.HttpStatusCode.OK);
    var sut = new NugetClient(new HttpClient(messageHandler));

    var response = await sut.GetDataAsync("mypackage");

    Assert.Contains(version, response.Versions);
}

In the happy case, I would like to make sure about two things: 'HTTP 200 OK' is returned and the version input string is set with the content of the response. The status code can be set with the WithStatusCode() method, while the content is set with the WithExpectedContent(). Note, that I am passing an actual NugetResponse object to the WithExpectedContent() method, but there are other overloads to take a more primitive response data too. In this case, the NugetResponse object gets serialized to JSON and used as the content of the response. The last step of the arrange section creates a new HttpClient, passing the FakeHttpMessageHandler object to the constructor. Finally, the system under test object is created by passing the mocked HttpClient to it. The rest of the test invokes the method under test, and asserts the results.

Conclusion

Re-use components which makes your life easier. Implementing a custom message handler for the 12th time might be a good Kata, but definitely avoidable where smart people has already figured out a good implementation. Testing HttpClient is not trivial at first sight. So do not spend effort on designing a helper class for tests, but rather improve the critical parts of the application, and re-use everything else given.