Stubbing the internet with Postman, HttpClient and HttpMessageHandler

Books I read
See all recommendations

Using Postman or Insomnia to test code and integrations

Recently Tim Heuer at Microsoft posted a poll on Twitter asking people using tools like Postman and Insomnia why they "just don't rely on test frameworks".

I wanted to pitch in with the method I use, which is a combination of both.

Whenever I integrate with third parties, I almost always start off with a spike to learn how their APIs work.
Right now for instance, I'm building some integrations with the norwegian business registry.
They have a couple of nice and simple endpoints. Here's a Postman request searching for our company, MarkedsPartner.

Searching for MarkedsPartner with Brreg APIs

I also had to run this code from a public webserver in order to find the right request messages from a second third party, so I threw in some HttpClient usage for the above, while logging incoming requests.

I could then replicate the requests in Postman to continue spiking with my mediating API controller.

As soon as I knew all about the incoming and outgoing messages, I was ready to start stabilizing with tests so I could proceed with refactoring and extending.

Stubbing the entire internet

The .NET HttpClient is fully stubbable, but the seam is fairly well hidden. I'm not convinced enough people know about this.
The HttpClient has a parameterized constructor taking the abstract HttpMessageHandler as its only parameter. By default this is a concrete HttpClientHandler when the parameterless ctor is used. Within HttpMessageHandler there is a protected SendAsync method which is the final method called by all the Get, Put, Post methods etc. It receives a HttpRequestMessage and returns a HttpResponseMessage.

You can use mocking frameworks like Moq to stub protected methods. But I really like to build my own fake HttpMessageHandler implementation for HTTP integration tests. In my experience it's actually (way) less code, and provides both stubbing and mocking mechanisms.

The simplest way to build it is to just return the same message for any call, but with a few extensions it's almost like a small stub internet with static resources. Here's how you could build one that varies its responses by request URI:

public class StubHttpClientHandler : HttpClientHandler
{
    public readonly Dictionary<string, HttpResponseMessage> UriResponses = new Dictionary<string, HttpResponseMessage>();

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var url = request.RequestUri.ToString();
        if (UriResponses.ContainsKey(url))
        {
            return await Task.FromResult(UriResponses[url]);
        }

        return await Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound));
    }
}

To make sure my production call hits this integration border instead of going off to the internet, I have to abstract away the creation of a HttpClient in my code. Often I set up a Func<HttpClient> factory with a DI container, but in my current case it's enough to add a static property to the SUT (system under test):

public static Func<HttpClient> HttpClientFactory = () => new HttpClient();

In my tests, I then swap out that factory with a function that returns a reference to a HttpClient using my StubHttpClientHandler:

var handler = new StubHttpClientHandler();
HttpClient httpClient = new HttpClient(handler);
BrregLookupController.HttpClientFactory = () => httpClient;

The final thing to do is to register a URI with a corresponding response message:

handler.UriResponses.Add(
    "https://data.brreg.no/enhetsregisteret/api/enheter?navn=markedspartner",
    new HttpResponseMessage(HttpStatusCode.OK)
    {
        Content = new StringContent("{\"status\":\"OK\"}", Encoding.UTF8)
    }
);

Whenever the controller now fires a call to that URL, it'll get a response with the JSON provided in the response message. You could add a content-type header and such for good measure, but it's not really necessary depending on how you handle the result.

Bringing Postman responses into the tests

Of course a slim JSON object with a status message isn't what I'm after here. I'm after the exact response the Brreg APIs would give me. Instead of pasting the entire JSON graph seen in the screenshot above, I like to keep them in files instead.

Those kinds of files are excellent candidates for Embedded Resources in the test project. So I paste the entire graph into a JSON file in a "TestResources" folder and make sure the file's build action is "Embedded Resource":

Example resource file in Visual Studio

In order to easily provide that content to my stub I usually employ a simple helper class for resources:

public static class TestData
{
    public static string ReadAsString(string resourceName, string suffix = "json")
    {
        using (var resourceStream = typeof(TestData).Assembly.GetManifestResourceStream(
            $"Full.TestProject.Namespace.TestResources.{resourceName}.{suffix}"))
        {
            if (resourceStream == null)
            {
                throw new InvalidOperationException($"Couldn't find resource '{resourceName}'");
            }
            return new StreamReader(resourceStream, Encoding.UTF8).ReadToEnd();
        }
    }
}

Now we can change the stub response as follows:

handler.UriResponses.Add(
    "https://data.brreg.no/enhetsregisteret/api/enheter?navn=markedspartner",
    new HttpResponseMessage(HttpStatusCode.OK)
    {
        Content = new StringContent(TestData.ReadAsString("NameSearchResult.MarkedsPartner"), Encoding.UTF8)
    }
);

Verifying the integration

As long as the SUT returns something serializable, in this case it's an API controller returning a strongly typed transformed version of the data from Brreg, we can verify the entire graph using ApprovalTests. I've blogged about approvaltests before if you don't know what it is. Here's the entire act / assert part of the test:

var response = await controller.Lookup(new LookupRequest
{
    Name = "MarkedsPartner"
});

Approvals.VerifyJson(JsonConvert.SerializeObject(response, JsonConfigurationAttribute.SerializerSettings));

I want to make sure I serialize exactly as the controller formatter will, so I pass the production serialization settings to ApprovalTests. On a sidenote, Approvals.VerifyJson works best with non-indented JSON, so make sure to use settings that have formatting set to None.

Expanding on the stubbed world wide web

There are a couple of other mechanisms I enjoy to use with the StubHttpClientHandler. First of all, we can use it as a mock to verify that our code sends out one-way messages as well. In order to do that we simply record every request in the SendAsync method:

public class StubHttpClientHandler : HttpClientHandler
{
    public readonly List<HttpRequestMessage> Requests = new List<HttpRequestMessage>();
    public readonly Dictionary<string, HttpResponseMessage> UriResponses = new Dictionary<string, HttpResponseMessage>();

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        Requests.Add(request);

        // ...
    }
}

We can then assert that URLs and even bodies were sent off by our code:

Assert.That(handler.Requests, Has.One
    .Property("RequestUri")
    .EqualTo(new Uri("https://data.brreg.no/enhetsregisteret/api/enheter?navn=MarkedsPartner"))
);

The second mechanism I enjoy is to provide predicates and response factories in order to simplify setup.
For my current code I haven't refined it very much, so it might look a bit complex, but I'm sure you can follow the code. 😊

public class StubHttpClientHandler : HttpClientHandler
{
    public readonly List<HttpRequestMessage> Requests = new List<HttpRequestMessage>();
    public readonly Dictionary<string, HttpResponseMessage> UriResponses = new Dictionary<string, HttpResponseMessage>();
    public readonly List<(Func<HttpRequestMessage, bool> predicate, Func<HttpRequestMessage, HttpResponseMessage> responseFactory)> PredicateResponses = 
        new List<(Func<HttpRequestMessage, bool> predicate, Func<HttpRequestMessage, HttpResponseMessage> responseFactory)>();

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        Requests.Add(request);

        // Return any match for full URIs
        var url = request.RequestUri.ToString();
        if (UriResponses.ContainsKey(url))
        {
            return await Task.FromResult(UriResponses[url]);
        }

        // Run all predicates and see if one matches
        var predicateEntry = PredicateResponses.FirstOrDefault(x => x.predicate(request));
        if (predicateEntry.responseFactory != null)
        {
            // Create the response using the provided factory
            return await Task.FromResult(predicateEntry.responseFactory(request));
        }

        return await Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound));
    }
}

Now we can set up a list of different responses as a dictionary, and then provide different responses based on the parameter. Here we could also add checks that authorization headers or other request requirements are verified by our production code:

Dictionary<string, Func<string>> searchResults = new Dictionary<string, Func<string>>
{
    { "MarkedsPartner", () => TestData.ReadAsString("NameSearchResult.MarkedsPartner")}
};

handler.PredicateResponses.Add((
    predicate: request => 
        request.RequestUri.ToString().StartsWith("https://data.brreg.no/enhetsregisteret/api/enheter?navn="),
    responseFactory: request =>
        searchResults.ContainsKey(request.RequestUri.ParseQueryString()["navn"])
            ? new HttpResponseMessage(HttpStatusCode.OK)
            {
                Content = new StringContent(searchResults[request.RequestUri.ParseQueryString()["navn"]](), Encoding.UTF8)
            }
            : new HttpResponseMessage(HttpStatusCode.NotFound)
));

We can now hide away the slightly unrefactored and ugly stubbing code, while focusing on the parameter / response pairs in the searchResults dictionary.

A search for "MarkedsPartner" will return the expected response, while a search for "Umbraco" for instance would return a 404 response.

Conclusion

Using Postman or Insomnia is really useful for spiking, and it's invaluable when we need stub data for our tests. By leveraging files and embedded resources, we can easily build strong sets of integration tests, stubbed at the very frontier before the code runs off to the internet.

Making sure your own API controllers behave correctly is a matter of verifying with your production binding and serialization settings. Then you won't have to call your own code via tools such as Postman or Insomnia.

This is all necessary for .NET Framework users. For light .NET Core solutions, it should be fairly simple to spin up an inproc webserver and do the entire HTTP process in a speedy manner. However, the stubbed version of the internet is gold here as well.

Hope you found the code interesting and thanks for reading!

Here's a copy of the complete StubHttpClientHandler described in this post:

public class StubHttpClientHandler : HttpClientHandler
{
    public readonly List<HttpRequestMessage> Requests = new List<HttpRequestMessage>();
    public readonly Dictionary<string, HttpResponseMessage> UriResponses = new Dictionary<string, HttpResponseMessage>();
    public readonly List<(Func<HttpRequestMessage, bool> predicate, Func<HttpRequestMessage, HttpResponseMessage> responseFactory)> PredicateResponses = 
        new List<(Func<HttpRequestMessage, bool> predicate, Func<HttpRequestMessage, HttpResponseMessage> responseFactory)>();

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        Requests.Add(request);

        var url = request.RequestUri.ToString();
        if (UriResponses.ContainsKey(url))
        {
            return await Task.FromResult(UriResponses[url]);
        }

        var predicateEntry = PredicateResponses.FirstOrDefault(x => x.predicate(request));
        if (predicateEntry.responseFactory != null)
        {
            return await Task.FromResult(predicateEntry.responseFactory(request));
        }

        return await Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound));
    }
}

Author

comments powered by Disqus