Mocking an HttpClient using ASP.NET Core TestServer

 
 
  • Gérald Barré

I've already written about mocking an HttpClient using an HttpClientHandler. You can write the HttpClientHandler yourself or use a mocking library. Multiple NuGet packages can help you write the HttpClientHandler such as Moq, RichardSzalay.MockHttp, HttpClientMockBuilder, SoloX.CodeQuality.Test.Helpers, WireMock.Net, etc.

Here's an example of MockHttp:

C#
var mockHttp = new MockHttpMessageHandler();
mockHttp.When("http://localhost/api/user/*")
        .Respond("application/json", "{'name' : 'Test McGee'}");

var client = mockHttp.ToHttpClient();

// Use the mocked HttpClient
var myToDoService = new MyToDoService(client);

You can see the library provides an easy way to configure the response for each request. But, this is a new syntax to learn and the way to create the response may be verbose and not easy to write. What if we could use ASP.NET Core to define the mock of the server and create an HttpClient for this server.

Using ASP.NET Core to create the fake server provides multiple advantages:

  • Good documentation
  • Support mocking HTTP and WebSocket
  • Using Minimal API, the syntax is very concise. Sometimes, more concise than other libraries…

ASP.NET Core provides a package to create a fake server using TestServer. This server implementation doesn't rely on the TCP stack and doesn't need to expose the server on a port. Instead, it relies on HttpClientHandler to bypass the network stack. To use ASP.NET Core from the test, you may need to add the FrameworkReference and include the NuGet package Microsoft.AspNetCore.TestHost:

TestProject.csproj (csproj (MSBuild project file))
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net7.0</TargetFramework>
  </PropertyGroup>

  <!-- Allow to use ASP.NET Core -->
  <ItemGroup>
    <FrameworkReference Include="Microsoft.AspNetCore.App" />
    <PackageReference Include="Microsoft.AspNetCore.TestHost" Version="7.0.1" />
  </ItemGroup>

  <!-- Reference xunit -->
  <ItemGroup>
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
    <PackageReference Include="xunit" Version="2.4.2" />
    <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
  </ItemGroup>
</Project>

Then, you can use configure the server and create an HttpClient:

C#
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;

public class UnitTest1
{
    [Fact]
    public async Task Test1()
    {
        // Configure and create HttpClient mock
        var builder = WebApplication.CreateBuilder();
        builder.WebHost.UseTestServer();
        await using var application = builder.Build();

        application.MapGet("/", () => "Hello meziantou").RequireHost("meziantou.net");
        application.MapGet("/", () => "Hello contoso").RequireHost("contoso.com");
        application.MapGet("/", () => "Hello localhost");
        application.MapGet("/{id}", (int id) => Results.Ok(new { id = id, name = "Sample" }));

        _ = application.RunAsync();
        using var httpClient = application.GetTestClient();

        // Use the HttpClient mock
        Assert.Equal("Hello localhost", await httpClient.GetStringAsync("/"));
        Assert.Equal("""{"id":10,"name":"Sample"}""", await httpClient.GetStringAsync("/10"));
        Assert.Equal("Hello meziantou", await httpClient.GetStringAsync("https://www.meziantou.net/"));
        Assert.Equal("Hello contoso", await httpClient.GetStringAsync("http://contoso.com/"));
    }
}

Some parts of the test should be encapsulated in a class to reduce the verbosity:

C#
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;

public class UnitTest1
{
    [Fact]
    public async Task Test1()
    {
        // Mock the ToDo service
        await using var context = new HttpClientMock();

        var todos = new ConcurrentDictionary<int, ToDo>();
        int nextId = 0;
        context.Application.MapGet("/", () => todos.Values.ToArray());
        context.Application.MapGet("/{id}", (int id) => todos.GetValueOrDefault(id));
        context.Application.MapPost("/", (ToDo todo) => todos.GetOrAdd(Interlocked.Increment(ref nextId), id => todo with { Id = id }));
        context.Application.MapDelete("/{id}", (int id) => todos.TryRemove(id, out _));
        using var httpClient = context.CreateHttpClient();

        // Use the HttpClient mock to instantiate the ToDo service
        var myTodoService = new TodoService(httpClient);
        var todo = await myTodoService.Save(new ToDo { Name = "Sample" });

        Assert.Equal(1, todo.Id);
        Assert.NotEmpty(await myTodoService.GetAll());
    }
}

class HttpClientMock : IAsyncDisposable
{
    private bool _running;

    public HttpClientMock()
    {
        var builder = WebApplication.CreateBuilder();
        builder.WebHost.UseTestServer();
        Application = builder.Build();
    }

    public WebApplication Application { get; }

    public HttpClient CreateHttpClient()
    {
        StartServer();
        return Application.GetTestClient();
    }

    private void StartServer()
    {
        if (!_running)
        {
            _running = true;
            _ = Application.RunAsync();
        }
    }

    public async ValueTask DisposeAsync() => await Application.DisposeAsync();
}

#Mocking a Typed HttpClient

It's common to use typed HttpClient in ASP.NET Core. Here's an example of a ToDoService:

C#
var builder = WebApplication.CreateBuilder(args);
services.AddHttpClient<ToDoService>(); // Register the HttpClient for the ToDoService
var app = builder.Build();

class ToDoService
{
    private readonly HttpClient _httpClient;

    // Inject the HttpClient in the ctor
    public ToDoService(HttpClient httpClient) => _httpClient = httpClient;

    public Task<ToDo[]> GetAll() => _httpClient.GetFromJsonAsync<ToDo[]>("/");
    }
}

You can mock the HttpClient using the same technique as above. But it's a bit harder as you need to mock more services:

C#
[Fact]
public async Task Test1()
{
    // Configure the HttpClient mock
    var builder = WebApplication.CreateBuilder();
    builder.WebHost.UseTestServer();
    await using var application = builder.Build();
    application.MapGet("/", () => "Hello");
    _ = application.RunAsync();
    var httpClientHandler = application.GetTestServer().CreateHandler()

    // Use the mock
    var handlers = new ConcurrentDictionary<string, HttpMessageHandler>()
    {
        [typeof(ToDoService).Name] = httpClientHandler,
    };
    await using var factory = new MyApplicationFactory(handlers);
    var service = factory.Services.GetRequiredService<ToDoService>();
    _ = await service.GetAll();
}

private sealed class MyApplicationFactory : WebApplicationFactory<Program>
{
    private readonly ConcurrentDictionary<string, HttpMessageHandler> _handlers;

    public MyApplicationFactory(ConcurrentDictionary<string, HttpMessageHandler> handlers)
        => _handlers = handlers;

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            services.AddTransient<HttpMessageHandlerBuilder>(services => new MockHttpMessageHandlerBuilder(_handlers));
        });
    }

    private sealed class MockHttpMessageHandlerBuilder : HttpMessageHandlerBuilder
    {
        private readonly ConcurrentDictionary<string, HttpMessageHandler> _handlers;

        public MockHttpMessageHandlerBuilder(ConcurrentDictionary<string, HttpMessageHandler> _handlers)
        {
            this._handlers = _handlers;
        }

        public override string? Name { get; set; }
        public override HttpMessageHandler PrimaryHandler { get; set; }
        public override IList<DelegatingHandler> AdditionalHandlers { get; } = new List<DelegatingHandler>();

        public override HttpMessageHandler Build()
        {
            if (Name != null && _handlers.TryGetValue(Name, out var handler))
                return CreateHandlerPipeline(handler, AdditionalHandlers);

            return CreateHandlerPipeline(PrimaryHandler, AdditionalHandlers);
        }
    }
}

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?Buy Me A Coffee💖 Sponsor on GitHub