In my last post I showed how to automatically generate a typed client for use with HttpClientFactory from a swagger file. Now I want to make changes to the client’s behaviour and need unit tests, in this post I will look at the HttpClient Interception library and how we can effectively use it in our tests.
Take a look at the following code that makes use of our previously generated client.
var pet = await petStoreClient.GetPetByIdAsync(1, ct);var firstTag = pet.Tags.FirstOrDefault();This looks fine, right? But what happens if Tags is null? Can that event happen?
The quick fix would be to use ?. to propagate null safety, but maybe we want our deserialization to never allow collections to be null, but to instead default it to an empty collection.
To figure out the current behaviour and to document how we’d like it to work, we should write tests.
I will be writing these tests in F#, ‘cos it rocks! Not only is it a really nice language, I want to use string literals containing JSON in my tests, and this is painful in C#.
HttpClient Interception
This library is really slick, it has a fluent API that can produce an HttpClient loaded with an HttpClientHandler that will intercept requests and return configured responses, so it’s great for unit testing.
The fluent API is really intuitive, you can see some great examples on the README:
module NSwag.PetStore.Client.Tests.Tests
open Systemopen Xunitopen NSwag.PetStore.Clientopen JustEat.HttpClientInterception
[<Fact>]let ``Given missing collection property, we deserialize to an empty collection. 🚀`` () =
let builder = HttpRequestInterceptionBuilder() .Requests().ForAnyHost().ForPath("/pet/1234") .Responds().WithContent """{ "id": 0, "name": "doggie", "photoUrls": [ "string" ], "status": "available"}"""
let options = HttpClientInterceptorOptions().ThrowsOnMissingRegistration() builder.RegisterWith options |> ignore use innerClient = options.CreateHttpClient()
innerClient.BaseAddress <- "http://test-host" |> Uri
let client = PetStoreClient(innerClient) let pet = client.GetPetByIdAsync(1234L) |> Async.AwaitTask |> Async.RunSynchronously
Assert.NotNull pet.Tags Assert.Empty pet.TagsSome things to call out:
- We’re using
xUnit, another great choice would be Expecto, however there’s no test runner yet for Rider, and xUnit will work just fine here. xUnit works pretty well with F#, we don’t have to have class definitions, notice how we have a module here, but as soon as we want to useITestOutputHelperwe have to switch to using classes. - Function names in F# can contain pretty much anything by escaping with double-ticks: spaces, punctuation, even emojis (which means you have to use them, surely!).
- String literals can contain
"when we use triple-quoted strings - this is great for JSON and XML snippets.
In terms of what we are doing with HttpClient Interceptor, it almost needs no explanation as it’s completely declarative, the only comment I’d add is that you’ll probably want the ThrowsOnMissingRegistration during tests, as this ensures everything is completely in-memory, and nothing passes through and materialises to an actual HTTP request .

Right, so if the Tag part of the JSON is omitted then we end up with a null collection, which is not what we want, so let’s change it.
NSwag lets us control the serialization settings in a few ways, one way is via the jsonSerializerSettingsTransformationMethod setting, it lets us point to a static method to configure the JsonSerializerSettings.
After lots of investigation, I found that this was the simplest way to achieve the deserialization that I wanted:
using SystemSystem;using SystemSystem.CollectionsCollections.GenericGeneric;using SystemSystem.ReflectionReflection;using Newtonsoft.Json;using Newtonsoft.Json.Serialization;
namespace NSwagNSwag.PetStorePetStore.ClientClient{ internal static class PetStoreSerializerSettingsPetStoreSerializerSettings { public static JsonSerializerSettings TransformSettings(JsonSerializerSettings settings) { settings.DefaultValueHandling = DefaultValueHandling.Populate; settings.ContractResolver = new NullToEmptyListResolverNullToEmptyListResolver(); return settings; } }
internal sealed class NullToEmptyListResolverNullToEmptyListResolver :NullToEmptyListResolver DefaultContractResolverNullToEmptyListResolver { protected override JsonPropertyJsonPropertyRepresents a single property for a JSON object. CreatePropertyJsonProperty NullToEmptyListResolver.CreateProperty(MemberInfo member, MemberSerialization memberSerialization)(MemberInfoMemberInfoObtains information about the attributes of a member and provides access to member metadata. memberMemberInfo member, MemberSerialization memberSerialization) { varJsonProperty NullToEmptyListResolver.CreateProperty(MemberInfo member, MemberSerialization memberSerialization) property = base.CreateProperty(memberMemberInfo member, memberSerialization);
varJsonProperty NullToEmptyListResolver.CreateProperty(MemberInfo member, MemberSerialization memberSerialization) propType = property.PropertyType; if (propType.IsGenericTypeJsonProperty NullToEmptyListResolver.CreateProperty(MemberInfo member, MemberSerialization memberSerialization) &&JsonProperty NullToEmptyListResolver.CreateProperty(MemberInfo member, MemberSerialization memberSerialization) propType.GetGenericTypeDefinitionJsonProperty NullToEmptyListResolver.CreateProperty(MemberInfo member, MemberSerialization memberSerialization)() ==JsonProperty NullToEmptyListResolver.CreateProperty(MemberInfo member, MemberSerialization memberSerialization) typeof(ICollectionICollection<>Defines methods to manipulate generic collections.<ICollection<>Defines methods to manipulate generic collections.>ICollection<>Defines methods to manipulate generic collections.)) property.NullValueHandlingJsonProperty NullToEmptyListResolver.CreateProperty(MemberInfo member, MemberSerialization memberSerialization) = NullValueHandlingJsonProperty NullToEmptyListResolver.CreateProperty(MemberInfo member, MemberSerialization memberSerialization).IncludeJsonProperty NullToEmptyListResolver.CreateProperty(MemberInfo member, MemberSerialization memberSerialization); return property; }
protected override IValueProvider CreateMemberValueProvider(MemberInfoMemberInfoObtains information about the attributes of a member and provides access to member metadata. memberMemberInfo member) { var provider = base.CreateMemberValueProvider(memberMemberInfo member);
if (memberMemberInfo member.MemberTypeMemberTypes MemberInfo.MemberTypeWhen overridden in a derived class, gets a MemberTypes value indicating the type of the member - method, constructor, event, and so on.ReturnsA MemberTypes value indicating the type of member. == MemberTypesMemberTypesMarks each type of member that is defined as a derived class of MemberInfo..PropertyMemberTypes.PropertySpecifies that the member is a property.) { varTypeRepresents type declarations: class types, interface types, array types, value types, enumeration types, type parameters, generic type definitions, and open or closed constructed generic types. propTypeType? propType = ((PropertyInfoPropertyInfoDiscovers the attributes of a property and provides access to property metadata.)memberMemberInfo member).PropertyTypeType PropertyInfo.PropertyTypeGets the type of this property.ReturnsThe type of this property.; if (propTypeType? propType.IsGenericTypebool Type.IsGenericTypeGets a value indicating whether the current type is a generic type.Returnsif the current type is a generic type; otherwise, . && propTypeType? propType.GetGenericTypeDefinitionType Type.GetGenericTypeDefinition()Returns a Type object that represents a generic type definition from which the current generic type can be constructed.ReturnsA Type object representing a generic type from which the current type can be constructed.ExceptionsInvalidOperationException — The current type is not a generic type. That is, IsGenericType returns .NotSupportedException — The invoked method is not supported in the base class. Derived classes must provide an implementation.() == typeof(ICollectionICollection<>Defines methods to manipulate generic collections.<ICollection<>Defines methods to manipulate generic collections.>ICollection<>Defines methods to manipulate generic collections.)) { return new EmptyListValueProviderNullToEmptyListResolver.EmptyListValueProvider(provider, propTypeType? propType); } }
return provider; }
class EmptyListValueProviderNullToEmptyListResolver.EmptyListValueProvider :NullToEmptyListResolver.EmptyListValueProvider IValueProviderNullToEmptyListResolver.EmptyListValueProvider { readonly IValueProviderNullToEmptyListResolver.EmptyListValueProvider innerProvider; readonly objectobjectSupports all classes in the .NET class hierarchy and provides low-level services to derived classes. This is the ultimate base class of all .NET classes; it is the root of the type hierarchy. defaultValueobject NullToEmptyListResolver.EmptyListValueProvider.defaultValue;
public EmptyListValueProviderNullToEmptyListResolver.EmptyListValueProvider.EmptyListValueProvider(IValueProvider innerProvider, Type listType)(IValueProvider innerProvider, TypeTypeRepresents type declarations: class types, interface types, array types, value types, enumeration types, type parameters, generic type definitions, and open or closed constructed generic types. listTypeType listType) { this.innerProvider = innerProvider; defaultValueobject NullToEmptyListResolver.EmptyListValueProvider.defaultValue = ArrayArrayProvides methods for creating, manipulating, searching, and sorting arrays, thereby serving as the base class for all arrays in the common language runtime..CreateInstanceArray Array.CreateInstance(Type elementType, int length)Creates a one-dimensional Array of the specified Type and length, with zero-based indexing.ParameterselementType — The Type of the Array to create.length — The size of the Array to create.ReturnsA new one-dimensional Array of the specified Type with the specified length, using zero-based indexing.ExceptionsArgumentNullException — elementType is .ArgumentException — elementType is not a valid Type.NotSupportedException — elementType is not supported. For example, Void is not supported. -or- elementType is an open generic type.ArgumentOutOfRangeException — length is less than zero.(listTypeType listType.GetGenericArgumentsType[] Type.GetGenericArguments()Returns an array of Type objects that represent the type arguments of a closed generic type or the type parameters of a generic type definition.ReturnsAn array of Type objects that represent the type arguments of a generic type. Returns an empty array if the current type is not a generic type.ExceptionsNotSupportedException — The invoked method is not supported in the base class. Derived classes must provide an implementation.()[Array Array.CreateInstance(Type elementType, int length)Creates a one-dimensional Array of the specified Type and length, with zero-based indexing.ParameterselementType — The Type of the Array to create.length — The size of the Array to create.ReturnsA new one-dimensional Array of the specified Type with the specified length, using zero-based indexing.ExceptionsArgumentNullException — elementType is .ArgumentException — elementType is not a valid Type.NotSupportedException — elementType is not supported. For example, Void is not supported. -or- elementType is an open generic type.ArgumentOutOfRangeException — length is less than zero.0]Array Array.CreateInstance(Type elementType, int length)Creates a one-dimensional Array of the specified Type and length, with zero-based indexing.ParameterselementType — The Type of the Array to create.length — The size of the Array to create.ReturnsA new one-dimensional Array of the specified Type with the specified length, using zero-based indexing.ExceptionsArgumentNullException — elementType is .ArgumentException — elementType is not a valid Type.NotSupportedException — elementType is not supported. For example, Void is not supported. -or- elementType is an open generic type.ArgumentOutOfRangeException — length is less than zero., 0); }
public voidvoidSpecifies a return value type for a method that does not return a value. SetValuevoid NullToEmptyListResolver.EmptyListValueProvider.SetValue(object target, object value)(objectobjectSupports all classes in the .NET class hierarchy and provides low-level services to derived classes. This is the ultimate base class of all .NET classes; it is the root of the type hierarchy. targetobject target, objectobjectSupports all classes in the .NET class hierarchy and provides low-level services to derived classes. This is the ultimate base class of all .NET classes; it is the root of the type hierarchy. valueobject value) => innerProvider.SetValuevoid NullToEmptyListResolver.EmptyListValueProvider.SetValue(object target, object value)(targetobject target, valueobject value ??void NullToEmptyListResolver.EmptyListValueProvider.SetValue(object target, object value) defaultValueobject NullToEmptyListResolver.EmptyListValueProvider.defaultValue); public objectobjectSupports all classes in the .NET class hierarchy and provides low-level services to derived classes. This is the ultimate base class of all .NET classes; it is the root of the type hierarchy. GetValueobject NullToEmptyListResolver.EmptyListValueProvider.GetValue(object target)(objectobjectSupports all classes in the .NET class hierarchy and provides low-level services to derived classes. This is the ultimate base class of all .NET classes; it is the root of the type hierarchy. targetobject target) => innerProvider.GetValueobject NullToEmptyListResolver.EmptyListValueProvider.GetValue(object target)(targetobject target) ??object NullToEmptyListResolver.EmptyListValueProvider.GetValue(object target) defaultValueobject NullToEmptyListResolver.EmptyListValueProvider.defaultValue; } }}If there is a simpler way (and I suspect there must be!) do let me know in the comments.
Now when we run the tests again…

Other Options with HttpClient Interception
In this case I wanted my test to contain a JSON string literal, but in most cases we could instead use the WithJsonContent method, which takes an object and serializes it for us (optionally with serialization settings). Ironically it would have been easier in C# if I wanted to do this, as anonymous objects hasn’t yet landed in F# (but it’s sooo close!).
A compelling alternative would have been to use an HTTP bundle file, which to quote the docs, “can be used to store the HTTP requests to intercept and [store] their corresponding responses as JSON”.
Closing
HttpClient Interception is the brainchild of Martin Costello, who’s always doing amazing things in the .NET Core community.
You can find all of the code in this post here:
Comments