dotnet mock remote http server for unit tests

There are cases when our 3rd party dependencies are not allowed to be mocked.

Usualy response is something like - just make a wrappers and technically it is correct and good approach.

But what if we wish not to have wrappers and keep everything small and tiny?

Here is one of possible approaches, suppose there is an 3rd party API, something like thisone:

[HttpGet("/people")]
public IEnumerable<Person> GetPeople() {}

[HttpPost("/people")]
public void SavePerson(Person person) {}

Also they have published client library, which is something like:

public sealed class PeopleClient {
    public PeopleClient(Uri uri) {}
    public IEnumerable<Person> GetPeople() {}
    public void SavePerson(Person person) {}
}

Because there is no interface and class is sealed we can not mock it.

And because we do not wish to make and wrapper around it lets play with our mocked http server

We gonna need testing library which will take care about all required dependencies

dotnet add package Microsoft.AspNetCore.Mvc.Testing

And here is sample test:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.Http;
using Xunit;

namespace DemoTests;

// dotnet add package Microsoft.AspNetCore.Mvc.Testing
public class FakeRemoteServer
{
    [Fact]
    public async Task RemoteServerCanBeMocked()
    {
        // our dummy storage
        var storage = new List<Person>();

        // our fake server
        using var server = new WebHostBuilder().UseKestrel(/*x => x.ListenLocalhost(8080)*/).Configure(app =>
        {
            app.Run(async context =>
            {
                if (context.Request.Method == HttpMethods.Get && context.Request.Path == "/people")
                {
                    await context.Response.WriteAsJsonAsync(storage);
                }
                else if (context.Request.Method == HttpMethods.Post && context.Request.Path == "/people")
                {
                    var person = await context.Request.ReadFromJsonAsync<Person>();
                    if (person == null)
                    {
                        context.Response.StatusCode = StatusCodes.Status400BadRequest;
                        await context.Response.WriteAsJsonAsync(new { message = "person is null" });
                    }
                    else
                    {
                        storage.Add(person);
                        context.Response.StatusCode = StatusCodes.Status201Created;
                    }
                }
                else
                {
                    context.Response.StatusCode = StatusCodes.Status404NotFound;
                    await context.Response.WriteAsync("Not found");
                }
            });

        }).Build();
        await server.StartAsync();
        // now we can pass this uri to what ever client
        var uri = new Uri(server.ServerFeatures.Get<IServerAddressesFeature>()!.Addresses.First()); // http.BaseAddress = new Uri("http://localhost:8080");

        var http = new HttpClient();
        http.BaseAddress = uri;

        // check that there is no people
        var list = await http.GetFromJsonAsync<IEnumerable<Person>>("/people");
        Assert.NotNull(list);
        Assert.Empty(list);

        // born person
        await http.PostAsJsonAsync("/people", new Person { Age = 7, Name = "Michael" });

        list = await http.GetFromJsonAsync<IEnumerable<Person>>("/people");
        Assert.NotNull(list);
        Assert.NotEmpty(list);
        Assert.Equal(list.First().Name, "Michael");
    }
}

public record Person
{
    public string Name { get; init; } = "";
    public int Age { get; init; }
}

With such approach we can mock any remote http server, yep we still need to implement its endpoints and it is not prove that everything will work with actual server but still allows for many cool things to be done in tests

Note that we can pass our own port to kestresl server, technically if no port is given and server is starting it should bind to any available random port which should allow to run multiplse such servers in parallel