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:

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
(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.JsonProperty
Represents 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.MemberInfo
Obtains 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.MemberInfo
Obtains 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.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.
==
(method) IValueProvider NSwag.PetStore.Client.NullToEmptyListResolver.CreateMemberValueProvider(System.Reflection.MemberInfo member)
MemberTypes
(enum) System.Reflection.MemberTypes
Marks each type of member that is defined as a derived class of MemberInfo.
.Property
(constant) System.Reflection.MemberTypes.Property
Specifies that the member is a property.
)
{
var
(class) System.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
(local variable) System.Type? propType
= ((PropertyInfo
(class) System.Reflection.PropertyInfo
Discovers the attributes of a property and provides access to property metadata.
)member
(parameter) System.Reflection.MemberInfo member
).PropertyType
(property) System.Type System.Reflection.PropertyInfo.PropertyType
Gets the type of this property.
The type of this property.
;
if (propType
(local variable) System.Type? propType
.IsGenericType
(property) bool System.Type.IsGenericType
Gets a value indicating whether the current type is a generic type.
if 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.
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.
() ==
(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) 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
(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.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
(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.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
(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.
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
(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.
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.
()[
(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.
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]
(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.
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
(struct) void
Specifies 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) 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
(parameter) object target
, object
(class) 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
(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) 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
(method) object NSwag.PetStore.Client.NullToEmptyListResolver.EmptyListValueProvider.GetValue(object target)
(object
(class) 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
(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…

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.