Introduction

In the last post we looked at installing Just Eat’s HttpClient Interception library and setting up the basic request to match a Get request. This showed off the basic premise of library and how to use it.

One of the fundamental cool concepts of the library is the ability to catch calls which have not been setup. I was contacted by Stuart Lang (https://twitter.com/stuartblang) (Thanks Stu!!) about this after publishing the first post and he suggested that some of the initial confusion with the library comes when developers find that the tests start to make “real” requests and they are confused as to why that happens.

Before we get further into the library I think it’s a great idea to explore further.

Catching the None Matches

When you setup the requests you want to match in your tests there is the assumption you know all of the requests your test is going to make at the time you’re writing them. You setup the matching requests and the specified return responses and your code processes and passes as expected. This is all well and good if you’re testing a process with a small number of uris.

However as the code evolves, gets refactored and new requirements come in new requests could be added to the code flow being tested. This isn’t bad however there is a level of understanding that the setup requires. This code change may not be written by someone who understands the integration tests. The developer who’s made the change runs all the tests, they all go green and they think “all good” and commits the code. The test then fails on the build server for an odd reason. Or the developer may notice some strange logs when running them locally later on. Or another developer maybe flying to see a client or on a train commuting (when we still did that!) and they fail with a network error. It shouldn’t be making a network request, what went wrong?

The default behavior of the library is to try and match the request being executed and if there is no match setup, it will attempt to make the actual request over the network. This in a testing scenario is bad!

So what we want to do is get the tests to fail if not all the requests required are matched. But how do we do this?

When we setup the HttpClientInterceptorOptions instance we need to make sure the ThrowOnMissingRegistration property is set to the true.

This can be done in 2 ways but it essentially is setting the same value.

Set the property directly

options.ThrowOnMissingRegistration = true;

You can achieve this by setting the property on the options instance directly.

Set through an Extension Method

Or you can use the helpful extension method ThrowsOnMissingRegistration() which is extending HttpClientInterceptorOptions and setting the property for you under the hood.

Either can be called on the instance after instantiation in a separate call.

options.ThrowsOnMissingRegistration();

Or call it directly when the interceptor options are being instantiated as then it’s clear and explicit what is going on and you won’t forget to do it!

var options = new HttpClientInterceptorOptions().ThrowsOnMissingRegistration();

Going Under the Hood

Now we’ve looked at the how we set it up let’s look at the how it works.

One aspect I love about open source projects is you get to read other peoples code. It allows you to try and get into how they were thinking when they, or their team, were trying to solve a certain problem. You can learn so much reading other peoples source code and I would highly recommend it.

Why do I say this? So, we’ve set the ThrowOnMissingRegistration property to true and now our tests are failing as expected. But why? What is the error trying to tell us? Well I think it’s important to understand what is going on now we’ve set the value and to do that we look at the code.

How does the property we’ve set on the options class relate to throwing an exception?

It comes down to the InterceptingHttpMessageHandler instance in the library, which is a DelegatingHandler, which is added into the HttpClient processing pipeline when you request a HttpClient instance from the library to use in your tests.

Now that sentence is quite a mouthful so let’s break it down a bit. When you create a HttpClient instance you can give it essentially “request handlers” in a pipeline setup. The request keeps going through the SendAsync method on each one down the pipe and then the response bubbles back up. It works in a very similar way to middleware. This InterceptingHttpMessageHandler instance is added into that pipeline.

Starting from https://github.com/justeat/httpclient-interception/blob/b608092cec60c130062758f299de087b82ebd917/src/HttpClientInterception/InterceptingHttpMessageHandler.cs#L50 it is in the SendAsync method where the logic flows.

        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            if (_options.OnSend != null)
            {
                await _options.OnSend(request).ConfigureAwait(false);
            }
            
            var response = await _options.GetResponseAsync(request, cancellationToken).ConfigureAwait(false);

            if (response == null && _options.OnMissingRegistration != null)
            {
                response = await _options.OnMissingRegistration(request).ConfigureAwait(false);
            }
            
            if (response != null)
            {
                return response;
            }
            
            if (_options.ThrowOnMissingRegistration)
            {
#pragma warning disable CA1062
                throw new HttpRequestNotInterceptedException(
                    $"No HTTP response is configured for {request.Method.Method} {request.RequestUri}.",
                    request);
#pragma warning restore CA1062
            }

            return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
        }

We can see above the SendAsync implementation steps through a number of processing points which aid with the library functionality.

It starts with OnSend delegate. You can setup a delegate to fire before each request is sent. This can be used to manipulate the request instance or allow for some specific logging in the test being executed for example.

The request then goes into the _options.GetResponseAsync method. This is where the matching processing using the HttpClientInterceptorOptions instance we had earlier in the test setup is performed. It is also where the configured response gets returned from a successful match. If there is a successful match the response is returned from the method.

If the matched response is null there is one last configurable option to use to make changes to it. This is the OnMissingRegistration delegate. This gives the developer an optional hook to interrogate the original HttpRequestMessage and optionally return a HttpResponseMessage.

However if there is no matched response or OnMissingRegistration response to return then the processing needs to determine what to do next. Should it fail the request as nothing has been matched or should the request continue down the “pipe” and be sent out to the real world? This is where the ThrowOnMissingRegistration property is used. As we can see from above if we’ve set this and there is no match response to return (or injected through the hook points) then an exception will be thrown and the test will fail. This is what we are trying to achieve!

Conclusion

In this post we have looked at how we can make it explicit that all uris in a test must be setup to pass when using the Just Eat HttpClient Interception library. This is done through setting the ThrowOnMissingRegistration property to true. It’s as straight forward as that. This will make sure your tests will fail when additional requests are added in at a later date and not fail silently or have strange behavior. I would highly recommend making sure this value is set on all your tests.

If you’ve liked this post please check out my lightning talk about the basics of the library. In future posts I hope to continue to explore the library more so please subscribe to my rss feed (link below) and reach out to me on Twitter if you have any comments.