Kiota Client Unit Testing: mocking data

After implementing Kiota clients with resilience, I felt the need to spend some time on unit testing them. The Kiota docs on unit testing uses NSubstitute to override the response of the adapter. After some testing (pun intended), I did not feel satisfied with the results; we should be able to use the "normal" way of testing an HttpClient, right?

Nick Chapsas makes an excellent point in his video titled "You are mocking the HttpClient the wrong way". He proposes to use the RichardSzalay.MockHttp package for some excellent mocking. In this blog, I'll show how to use the package together with Kiota.

Test using JSON strings

The easiest way to make the Kiota client testable, is to inject the string value of the JSON into the HTTP client using the RichardSzalay.MockHttp package:

[Fact]
public async Task KiotaPetStoreClientWithString()
{
    // arrange
    var json = @"
    [
      {
        ""id"": 42,
        ""category"": { ""id"": 1, ""name"": ""dog"" },
        ""name"": ""Bandit Heeler"",
        ""photoUrls"": [""https://upload.wikimedia.org/wikipedia/en/9/90/Bandit_Heeler.png""],
        ""tags"": [ { ""id"": 1, ""name"": ""cartoon"" } ],
        ""status"": ""available""
      },
      {
        ""id"": 1337,
        ""category"": { ""id"": 1, ""name"": ""dog"" },
        ""name"": ""Scooby-Doo"",
        ""photoUrls"": [""https://upload.wikimedia.org/wikipedia/en/5/53/Scooby-Doo.png""],
        ""tags"": [ { ""id"": 1, ""name"": ""cartoon"" } ],
        ""status"": ""available""
      }
    ]";

    var mockHttp = new MockHttpMessageHandler();
    mockHttp
        .When(HttpMethod.Get, "/v2/pet/findByStatus?status=available")
        .Respond(HttpStatusCode.OK, "application/json", json);

    var client = new PetStoreClient(
        new DefaultRequestAdapter(
            new AnonymousAuthenticationProvider(),
            httpClient: mockHttp.ToHttpClient()
        )
    );

    // act
    var result = await client.Pet.FindByStatus.GetAsync(x =>
    {
        x.QueryParameters.Status = [GetStatusQueryParameterType.Available];
    });

    // assert
    result.Should().NotBeNull();
    result.Count.Should().Be(2);

    result[0].Id.Should().Be(42);
    result[0].Name.Should().Be("Bandit Heeler");

    result[1].Id.Should().Be(1337);
    result[1].Name.Should().Be("Scooby-Doo");
}

Test using objects

We can do one better... by using objects. There is just one caveat; you need to do the serialization yourself (so basically you're still doing strings).

[Fact]
public async Task KiotaPetStoreClientWithMockedObjects()
{
    // arrange
    var tag = new Tag { Id = 1, Name = "cartoon" };
    var category = new Category { Id = 1, Name = "Dogs" };
    var pets = new Pet[] {
        new Pet
        {
            Id = 42,
            Name = "Bandit Heeler",
            Category = category,
            PhotoUrls = [ "https://upload.wikimedia.org/wikipedia/en/9/90/Bandit_Heeler.png" ],
            Tags = [ tag ],
            Status = Pet_status.Available
        },
        new Pet
        {
            Id = 1337,
            Name = "Scooby-Doo",
            Category = category,
            PhotoUrls = [ "https://upload.wikimedia.org/wikipedia/en/5/53/Scooby-Doo.png" ],
            Tags = [ tag ],
            Status = Pet_status.Available
        }
    };

    var serializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web);
    serializerOptions.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase));

    var jsonData = JsonSerializer.Serialize(pets, serializerOptions);

    var mockHttp = new MockHttpMessageHandler();
    mockHttp
        .When(HttpMethod.Get, "/v2/pet/findByStatus?status=available")
        .Respond(HttpStatusCode.OK, "application/json", jsonData);

    var client = new PetStoreClient(
        new DefaultRequestAdapter(
            new AnonymousAuthenticationProvider(),
            httpClient: mockHttp.ToHttpClient()
        )
    );

    // act
    var result = await client.Pet.FindByStatus.GetAsync(x =>
    {
        x.QueryParameters.Status = [GetStatusQueryParameterType.Available];
    });

    // assert
    result.Should().NotBeNull();
    result.Count.Should().Be(2);

    result[0].Id.Should().Be(42);
    result[0].Name.Should().Be("Bandit Heeler");

    result[1].Id.Should().Be(1337);
    result[1].Name.Should().Be("Scooby-Doo");
}

Bundling it together

I like the fact that we reuse the data objects that are created by the Kiota generator. Now, we could create a MockedPetStoreClientFactory that can act as a factory and a wrapper, making the calls testable. Observe the following:

using Ktt.Resilience.Clients.Kiota.HttpClients.PetStore;
using Ktt.Resilience.Clients.Kiota.HttpClients.PetStore.Models;
using Microsoft.Kiota.Abstractions.Authentication;
using Microsoft.Kiota.Bundle;
using RichardSzalay.MockHttp;
using System.Net;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Ktt.Resilience.Tests.Mocks;

public class MockedPetStoreClientFactory
{
    private readonly JsonSerializerOptions _serializerOptions;

    public List<Pet> Pets { get; set; } = [];

    public MockedPetStoreClientFactory()
    {
        _serializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web);
        _serializerOptions.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase));
    }

    public PetStoreClient CreateClient()
    {
        string SerializePets(Pet_status status)
        {
            var pets = Pets.Where(x => x.Status == status);
            return JsonSerializer.Serialize(pets, _serializerOptions);
        }

        var mockHttp = new MockHttpMessageHandler();

        mockHttp
            .When(HttpMethod.Get, "/v2/pet/findByStatus?status=available")
            .Respond(HttpStatusCode.OK, "application/json", SerializePets(Pet_status.Available));

        mockHttp
            .When(HttpMethod.Get, "/v2/pet/findByStatus?status=pending")
            .Respond(HttpStatusCode.OK, "application/json", SerializePets(Pet_status.Pending));

        mockHttp
            .When(HttpMethod.Get, "/v2/pet/findByStatus?status=sold")
            .Respond(HttpStatusCode.OK, "application/json", SerializePets(Pet_status.Sold));

        var client = new PetStoreClient(
            new DefaultRequestAdapter(
                new AnonymousAuthenticationProvider(),
                httpClient: mockHttp.ToHttpClient()
            )
        );

        return client;

    }
}

Now that we have a factory, it becomes easier to focus on the actual testing:

[Fact]
public async Task KiotaPetStoreMockedClient()
{
    // arrange
    var tag = new Tag { Id = 1, Name = "cartoon" };
    var category = new Category { Id = 1, Name = "Dogs" };
    var pets = new Pet[] {
        new Pet
        {
            Id = 42,
            Name = "Bandit Heeler",
            Category = category,
            PhotoUrls = [ "https://upload.wikimedia.org/wikipedia/en/9/90/Bandit_Heeler.png" ],
            Tags = [ tag ],
            Status = Pet_status.Available
        },
        new Pet
        {
            Id = 1337,
            Name = "Scooby-Doo",
            Category = category,
            PhotoUrls = [ "https://upload.wikimedia.org/wikipedia/en/5/53/Scooby-Doo.png" ],
            Tags = [ tag ],
            Status = Pet_status.Pending
        },
        new Pet
        {
            Id = 1950,
            Name = "Snoopy",
            Category = category,
            PhotoUrls = [ "https://upload.wikimedia.org/wikipedia/en/5/53/Snoopy_Peanuts.png" ],
            Tags = [ tag ],
            Status = Pet_status.Sold
        }
    };

    var mock = new MockedPetStoreClientFactory
    {
        Pets = [.. pets]
    };

    var client = mock.CreateClient();

    // act
    var availablePets = await client.Pet.FindByStatus.GetAsync(x =>
    {
        x.QueryParameters.Status = [GetStatusQueryParameterType.Available];
    });
    var pendingPets = await client.Pet.FindByStatus.GetAsync(x =>
    {
        x.QueryParameters.Status = [GetStatusQueryParameterType.Pending];
    });
    var soldPets = await client.Pet.FindByStatus.GetAsync(x =>
    {
        x.QueryParameters.Status = [GetStatusQueryParameterType.Sold];
    });

    // assert
    availablePets.Should().HaveCount(1);
    availablePets[0].Id.Should().Be(42);

    pendingPets.Should().HaveCount(1);
    pendingPets[0].Id.Should().Be(1337);

    soldPets.Should().HaveCount(1);
    soldPets[0].Id.Should().Be(1950);
}

How about dependency injection?

This MockedPetStoreClientFactory makes it also really easy to override dependency injection. You can register the client as a factory in the service collection.

[Fact]
public async Task KiotaPetStoreDependencyInjection()
{
    // arrange
    var factory = new MockedPetStoreClientFactory
    {
        Pets = {
            new Pet
            {
                Id = 42,
                Name = "Bandit Heeler",
                Category = new Category { Id = 1, Name = "Dogs" },
                PhotoUrls = [ "https://upload.wikimedia.org/wikipedia/en/9/90/Bandit_Heeler.png" ],
                Tags = [ new Tag { Id = 1, Name = "cartoon" } ],
                Status = Pet_status.Available
            }
        }
    };

    var services = new ServiceCollection();
    services.AddSingleton(x => factory.CreateClient());

    using var provider = services.BuildServiceProvider();
    var client = provider.GetRequiredService<PetStoreClient>();

    // act
    var availablePets = await client.Pet.FindByStatus.GetAsync(x =>
    {
        x.QueryParameters.Status = [GetStatusQueryParameterType.Available];
    });

    // assert
    availablePets.Should().HaveCount(1);
    availablePets[0].Id.Should().Be(42);
}

This makes API testing way easier.

Final thoughts

There are a lot of options. As long as you stick to the usage of the HttpClient, the word is your oyster. For more information on Kiota and resilience, check Kiota, dependency injection and resilience or Implementing HTTP Resilience by Microsoft.

The code is on GitHub, so check it out: code gallery / 12. HTTP and resilience.

expand_less brightness_auto