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:

justeat/httpclient-interception
A .NET Standard library for intercepting server-side HTTP dependencies - justeat/httpclient-interception
module NSwag.PetStore.Client.Tests.Tests
open System
open Xunit
open NSwag.PetStore.Client
open 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.Tags

Some 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 use ITestOutputHelper we 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 .

As we suspected!

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
System
;
using System
System
.Collections
Collections
.Generic
Generic
;
using System
System
.Reflection
Reflection
;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
namespace NSwag
NSwag
.PetStore
PetStore
.Client
Client
{
internal static class PetStoreSerializerSettings
PetStoreSerializerSettings
{
public static JsonSerializerSettings TransformSettings(JsonSerializerSettings settings)
{
settings.DefaultValueHandling = DefaultValueHandling.Populate;
settings.ContractResolver = new NullToEmptyListResolver
NullToEmptyListResolver
();
return settings;
}
}
internal sealed class NullToEmptyListResolver
NullToEmptyListResolver
:
NullToEmptyListResolver
DefaultContractResolver
NullToEmptyListResolver
{
protected override JsonProperty
JsonProperty
Represents a single property for a JSON object.
CreateProperty
JsonProperty NullToEmptyListResolver.CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
(MemberInfo
MemberInfo
Obtains information about the attributes of a member and provides access to member metadata.
member
MemberInfo member
, MemberSerialization memberSerialization)
{
var
JsonProperty NullToEmptyListResolver.CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
property = base.CreateProperty(member
MemberInfo member
, memberSerialization);
var
JsonProperty NullToEmptyListResolver.CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
propType = property.PropertyType;
if (propType.IsGenericType
JsonProperty NullToEmptyListResolver.CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
&&
JsonProperty NullToEmptyListResolver.CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
propType.GetGenericTypeDefinition
JsonProperty NullToEmptyListResolver.CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
() ==
JsonProperty NullToEmptyListResolver.CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
typeof(ICollection
ICollection<>
Defines methods to manipulate generic collections.
<
ICollection<>
Defines methods to manipulate generic collections.
>
ICollection<>
Defines methods to manipulate generic collections.
))
property.NullValueHandling
JsonProperty NullToEmptyListResolver.CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
= NullValueHandling
JsonProperty NullToEmptyListResolver.CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
.Include
JsonProperty NullToEmptyListResolver.CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
;
return property;
}
protected override IValueProvider CreateMemberValueProvider(MemberInfo
MemberInfo
Obtains information about the attributes of a member and provides access to member metadata.
member
MemberInfo member
)
{
var provider = base.CreateMemberValueProvider(member
MemberInfo member
);
if (member
MemberInfo member
.MemberType
MemberTypes MemberInfo.MemberType
When overridden in a derived class, gets a MemberTypes value indicating the type of the member - method, constructor, event, and so on.
A MemberTypes value indicating the type of member.
== MemberTypes
MemberTypes
Marks each type of member that is defined as a derived class of MemberInfo.
.Property
MemberTypes.Property
Specifies that the member is a property.
)
{
var
Type
Represents 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
Type? propType
= ((PropertyInfo
PropertyInfo
Discovers the attributes of a property and provides access to property metadata.
)member
MemberInfo member
).PropertyType
Type PropertyInfo.PropertyType
Gets the type of this property.
The type of this property.
;
if (propType
Type? propType
.IsGenericType
bool Type.IsGenericType
Gets a value indicating whether the current type is a generic type.
if the current type is a generic type; otherwise, .
&&
propType
Type? propType
.GetGenericTypeDefinition
Type Type.GetGenericTypeDefinition()
Returns a Type object that represents a generic type definition from which the current generic type can be constructed.
A Type object representing a generic type from which the current type can be constructed.
InvalidOperationException — 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(ICollection
ICollection<>
Defines methods to manipulate generic collections.
<
ICollection<>
Defines methods to manipulate generic collections.
>
ICollection<>
Defines methods to manipulate generic collections.
))
{
return new EmptyListValueProvider
NullToEmptyListResolver.EmptyListValueProvider
(provider, propType
Type? propType
);
}
}
return provider;
}
class EmptyListValueProvider
NullToEmptyListResolver.EmptyListValueProvider
:
NullToEmptyListResolver.EmptyListValueProvider
IValueProvider
NullToEmptyListResolver.EmptyListValueProvider
{
readonly IValueProvider
NullToEmptyListResolver.EmptyListValueProvider
innerProvider;
readonly object
object
Supports 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
object NullToEmptyListResolver.EmptyListValueProvider.defaultValue
;
public EmptyListValueProvider
NullToEmptyListResolver.EmptyListValueProvider.EmptyListValueProvider(IValueProvider innerProvider, Type listType)
(IValueProvider innerProvider, Type
Type
Represents 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
Type listType
)
{
this.innerProvider = innerProvider;
defaultValue
object NullToEmptyListResolver.EmptyListValueProvider.defaultValue
= Array
Array
Provides methods for creating, manipulating, searching, and sorting arrays, thereby serving as the base class for all arrays in the common language runtime.
.CreateInstance
Array Array.CreateInstance(Type elementType, int length)
Creates a one-dimensional Array of the specified Type and length, with zero-based indexing.
elementType — The Type of the Array to create.
length — The size of the Array to create.
A new one-dimensional Array of the specified Type with the specified length, using zero-based indexing.
ArgumentNullException — 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
Type listType
.GetGenericArguments
Type[] 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.
An 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.
NotSupportedException — 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.
elementType — The Type of the Array to create.
length — The size of the Array to create.
A new one-dimensional Array of the specified Type with the specified length, using zero-based indexing.
ArgumentNullException — 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.
elementType — The Type of the Array to create.
length — The size of the Array to create.
A new one-dimensional Array of the specified Type with the specified length, using zero-based indexing.
ArgumentNullException — 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
void
Specifies a return value type for a method that does not return a value.
SetValue
void NullToEmptyListResolver.EmptyListValueProvider.SetValue(object target, object value)
(object
object
Supports 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
object target
, object
object
Supports 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
object value
) => innerProvider.SetValue
void NullToEmptyListResolver.EmptyListValueProvider.SetValue(object target, object value)
(target
object target
, value
object value
??
void NullToEmptyListResolver.EmptyListValueProvider.SetValue(object target, object value)
defaultValue
object NullToEmptyListResolver.EmptyListValueProvider.defaultValue
);
public object
object
Supports 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
object NullToEmptyListResolver.EmptyListValueProvider.GetValue(object target)
(object
object
Supports 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
object target
) => innerProvider.GetValue
object NullToEmptyListResolver.EmptyListValueProvider.GetValue(object target)
(target
object target
) ??
object NullToEmptyListResolver.EmptyListValueProvider.GetValue(object target)
defaultValue
object 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…

That's better 🙂

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:

slang25/nswag-petstore-client
Contribute to slang25/nswag-petstore-client development by creating an account on GitHub.