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(method) <top-level-statements-entry-point> pet(local variable) ? pet = await petStoreClient(local variable) ? pet.GetPetByIdAsync(local variable) ? pet(1, ct(local variable) ? pet);var(method) <top-level-statements-entry-point> firstTag(local variable) ? firstTag = pet(local variable) ? pet.Tags(local variable) ? firstTag.FirstOrDefault(local variable) ? firstTag();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 System(namespace) System;using System(namespace) System.Collections(namespace) System.Collections.Generic(namespace) System.Collections.Generic;using System(namespace) System.Reflection(namespace) System.Reflection;using Newtonsoft.Json;using Newtonsoft.Json.Serialization;
namespace NSwag(namespace) NSwag.PetStore(namespace) NSwag.PetStore.Client(namespace) NSwag.PetStore.Client{ internal static class PetStoreSerializerSettings(class) NSwag.PetStore.Client.PetStoreSerializerSettings { public static JsonSerializerSettings(method) JsonSerializerSettings NSwag.PetStore.Client.PetStoreSerializerSettings.TransformSettings(JsonSerializerSettings settings) TransformSettings(method) JsonSerializerSettings NSwag.PetStore.Client.PetStoreSerializerSettings.TransformSettings(JsonSerializerSettings settings)(JsonSerializerSettings(parameter) JsonSerializerSettings settings settings(parameter) JsonSerializerSettings settings) { settings(parameter) JsonSerializerSettings settings.DefaultValueHandling(method) JsonSerializerSettings NSwag.PetStore.Client.PetStoreSerializerSettings.TransformSettings(JsonSerializerSettings settings) = DefaultValueHandling(method) JsonSerializerSettings NSwag.PetStore.Client.PetStoreSerializerSettings.TransformSettings(JsonSerializerSettings settings).Populate(method) JsonSerializerSettings NSwag.PetStore.Client.PetStoreSerializerSettings.TransformSettings(JsonSerializerSettings settings); settings(parameter) JsonSerializerSettings settings.ContractResolver(method) JsonSerializerSettings NSwag.PetStore.Client.PetStoreSerializerSettings.TransformSettings(JsonSerializerSettings settings) = new NullToEmptyListResolver(class) NSwag.PetStore.Client.NullToEmptyListResolver(); return settings(parameter) JsonSerializerSettings settings; } }
internal sealed class NullToEmptyListResolver(class) NSwag.PetStore.Client.NullToEmptyListResolver :(class) NSwag.PetStore.Client.NullToEmptyListResolver DefaultContractResolver(class) NSwag.PetStore.Client.NullToEmptyListResolver { protected override JsonProperty(struct) System.Text.Json.JsonPropertyRepresents a single property for a JSON object. CreateProperty(method) System.Text.Json.JsonProperty NSwag.PetStore.Client.NullToEmptyListResolver.CreateProperty(System.Reflection.MemberInfo member, MemberSerialization memberSerialization)(MemberInfo(class) System.Reflection.MemberInfoObtains information about the attributes of a member and provides access to member metadata. member(parameter) System.Reflection.MemberInfo member, MemberSerialization(parameter) MemberSerialization memberSerialization memberSerialization(parameter) MemberSerialization memberSerialization) { var(method) System.Text.Json.JsonProperty NSwag.PetStore.Client.NullToEmptyListResolver.CreateProperty(System.Reflection.MemberInfo member, MemberSerialization memberSerialization) property(local variable) ? property = base(local variable) ? property.CreateProperty(local variable) ? property(member(parameter) System.Reflection.MemberInfo member, memberSerialization(parameter) MemberSerialization memberSerialization);
var(method) System.Text.Json.JsonProperty NSwag.PetStore.Client.NullToEmptyListResolver.CreateProperty(System.Reflection.MemberInfo member, MemberSerialization memberSerialization) propType(local variable) ? propType = property(local variable) ? property.PropertyType(local variable) ? propType; if (propType(local variable) ? propType.IsGenericType(method) System.Text.Json.JsonProperty NSwag.PetStore.Client.NullToEmptyListResolver.CreateProperty(System.Reflection.MemberInfo member, MemberSerialization memberSerialization) &&(method) System.Text.Json.JsonProperty NSwag.PetStore.Client.NullToEmptyListResolver.CreateProperty(System.Reflection.MemberInfo member, MemberSerialization memberSerialization) propType(local variable) ? propType.GetGenericTypeDefinition(method) System.Text.Json.JsonProperty NSwag.PetStore.Client.NullToEmptyListResolver.CreateProperty(System.Reflection.MemberInfo member, MemberSerialization memberSerialization)() ==(method) System.Text.Json.JsonProperty NSwag.PetStore.Client.NullToEmptyListResolver.CreateProperty(System.Reflection.MemberInfo member, MemberSerialization memberSerialization) typeof(ICollection(interface) System.Collections.Generic.ICollection<>Defines methods to manipulate generic collections.<(interface) System.Collections.Generic.ICollection<>Defines methods to manipulate generic collections.>(interface) System.Collections.Generic.ICollection<>Defines methods to manipulate generic collections.)) property(local variable) ? property.NullValueHandling(method) System.Text.Json.JsonProperty NSwag.PetStore.Client.NullToEmptyListResolver.CreateProperty(System.Reflection.MemberInfo member, MemberSerialization memberSerialization) = NullValueHandling(method) System.Text.Json.JsonProperty NSwag.PetStore.Client.NullToEmptyListResolver.CreateProperty(System.Reflection.MemberInfo member, MemberSerialization memberSerialization).Include(method) System.Text.Json.JsonProperty NSwag.PetStore.Client.NullToEmptyListResolver.CreateProperty(System.Reflection.MemberInfo member, MemberSerialization memberSerialization); return property(local variable) ? property; }
protected override IValueProvider(method) IValueProvider NSwag.PetStore.Client.NullToEmptyListResolver.CreateMemberValueProvider(System.Reflection.MemberInfo member) CreateMemberValueProvider(method) IValueProvider NSwag.PetStore.Client.NullToEmptyListResolver.CreateMemberValueProvider(System.Reflection.MemberInfo member)(MemberInfo(class) System.Reflection.MemberInfoObtains information about the attributes of a member and provides access to member metadata. member(parameter) System.Reflection.MemberInfo member) { var(method) IValueProvider NSwag.PetStore.Client.NullToEmptyListResolver.CreateMemberValueProvider(System.Reflection.MemberInfo member) provider(local variable) ? provider = base(local variable) ? provider.CreateMemberValueProvider(local variable) ? provider(member(parameter) System.Reflection.MemberInfo member);
if (member(parameter) System.Reflection.MemberInfo member.MemberType(property) System.Reflection.MemberTypes System.Reflection.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. ==(method) IValueProvider NSwag.PetStore.Client.NullToEmptyListResolver.CreateMemberValueProvider(System.Reflection.MemberInfo member) MemberTypes(enum) System.Reflection.MemberTypesMarks each type of member that is defined as a derived class of MemberInfo..Property(constant) System.Reflection.MemberTypes.PropertySpecifies that the member is a property.) { var(class) System.TypeRepresents type declarations: class types, interface types, array types, value types, enumeration types, type parameters, generic type definitions, and open or closed constructed generic types. propType(local variable) System.Type? propType = ((PropertyInfo(class) System.Reflection.PropertyInfoDiscovers the attributes of a property and provides access to property metadata.)member(parameter) System.Reflection.MemberInfo member).PropertyType(property) System.Type System.Reflection.PropertyInfo.PropertyTypeGets the type of this property.ReturnsThe type of this property.; if (propType(local variable) System.Type? propType.IsGenericType(property) bool System.Type.IsGenericTypeGets a value indicating whether the current type is a generic type.Returnsif the current type is a generic type; otherwise, . &&(method) IValueProvider NSwag.PetStore.Client.NullToEmptyListResolver.CreateMemberValueProvider(System.Reflection.MemberInfo member) propType(local variable) System.Type? propType.GetGenericTypeDefinition(method) System.Type System.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.() ==(method) IValueProvider NSwag.PetStore.Client.NullToEmptyListResolver.CreateMemberValueProvider(System.Reflection.MemberInfo member) typeof(ICollection(interface) System.Collections.Generic.ICollection<>Defines methods to manipulate generic collections.<(interface) System.Collections.Generic.ICollection<>Defines methods to manipulate generic collections.>(interface) System.Collections.Generic.ICollection<>Defines methods to manipulate generic collections.)) { return new EmptyListValueProvider(class) NSwag.PetStore.Client.NullToEmptyListResolver.EmptyListValueProvider(provider(local variable) ? provider, propType(local variable) System.Type? propType); } }
return provider(local variable) ? provider; }
class EmptyListValueProvider(class) NSwag.PetStore.Client.NullToEmptyListResolver.EmptyListValueProvider :(class) NSwag.PetStore.Client.NullToEmptyListResolver.EmptyListValueProvider IValueProvider(class) NSwag.PetStore.Client.NullToEmptyListResolver.EmptyListValueProvider { readonly IValueProvider(class) NSwag.PetStore.Client.NullToEmptyListResolver.EmptyListValueProvider innerProvider(field) IValueProvider NSwag.PetStore.Client.NullToEmptyListResolver.EmptyListValueProvider.innerProvider; readonly object(class) objectSupports 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. defaultValue(field) object NSwag.PetStore.Client.NullToEmptyListResolver.EmptyListValueProvider.defaultValue;
public EmptyListValueProvider(method) NSwag.PetStore.Client.NullToEmptyListResolver.EmptyListValueProvider.EmptyListValueProvider(IValueProvider innerProvider, System.Type listType)(IValueProvider(parameter) IValueProvider innerProvider innerProvider(parameter) IValueProvider innerProvider, Type(class) System.TypeRepresents type declarations: class types, interface types, array types, value types, enumeration types, type parameters, generic type definitions, and open or closed constructed generic types. listType(parameter) System.Type listType) { this(field) IValueProvider NSwag.PetStore.Client.NullToEmptyListResolver.EmptyListValueProvider.innerProvider.innerProvider(field) IValueProvider NSwag.PetStore.Client.NullToEmptyListResolver.EmptyListValueProvider.innerProvider = innerProvider(parameter) IValueProvider innerProvider; defaultValue(field) object NSwag.PetStore.Client.NullToEmptyListResolver.EmptyListValueProvider.defaultValue = Array(class) System.ArrayProvides methods for creating, manipulating, searching, and sorting arrays, thereby serving as the base class for all arrays in the common language runtime..CreateInstance(method) System.Array System.Array.CreateInstance(System.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.(listType(parameter) System.Type listType.GetGenericArguments(method) System.Type[] System.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.()[(method) System.Array System.Array.CreateInstance(System.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](method) System.Array System.Array.CreateInstance(System.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 void(struct) voidSpecifies a return value type for a method that does not return a value. SetValue(method) void NSwag.PetStore.Client.NullToEmptyListResolver.EmptyListValueProvider.SetValue(object target, object value)(object(class) objectSupports 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. target(parameter) object target, object(class) objectSupports 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. value(parameter) object value) =>(method) void NSwag.PetStore.Client.NullToEmptyListResolver.EmptyListValueProvider.SetValue(object target, object value) innerProvider(field) IValueProvider NSwag.PetStore.Client.NullToEmptyListResolver.EmptyListValueProvider.innerProvider.SetValue(method) void NSwag.PetStore.Client.NullToEmptyListResolver.EmptyListValueProvider.SetValue(object target, object value)(target(parameter) object target, value(parameter) object value ??(method) void NSwag.PetStore.Client.NullToEmptyListResolver.EmptyListValueProvider.SetValue(object target, object value) defaultValue(field) object NSwag.PetStore.Client.NullToEmptyListResolver.EmptyListValueProvider.defaultValue); public object(class) objectSupports 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. GetValue(method) object NSwag.PetStore.Client.NullToEmptyListResolver.EmptyListValueProvider.GetValue(object target)(object(class) objectSupports 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. target(parameter) object target) =>(method) object NSwag.PetStore.Client.NullToEmptyListResolver.EmptyListValueProvider.GetValue(object target) innerProvider(field) IValueProvider NSwag.PetStore.Client.NullToEmptyListResolver.EmptyListValueProvider.innerProvider.GetValue(method) object NSwag.PetStore.Client.NullToEmptyListResolver.EmptyListValueProvider.GetValue(object target)(target(parameter) object target) ??(method) object NSwag.PetStore.Client.NullToEmptyListResolver.EmptyListValueProvider.GetValue(object target) defaultValue(field) object NSwag.PetStore.Client.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