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);

The library provides an easy way to configure responses per request. However, it requires learning a new syntax, and crafting responses can be verbose. A cleaner alternative is to use ASP.NET Core to define the fake server and create an HttpClient for it.

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

  • Well-documented API
  • Supports mocking HTTP and WebSocket
  • Minimal API syntax is concise, often more so than dedicated mocking libraries

ASP.NET Core provides a package to create a fake server using TestServer. This server implementation bypasses the TCP stack entirely and does not need to expose a port. Instead, it uses HttpClientHandler to bypass the network layer. To use ASP.NET Core from your test project, add the FrameworkReference and the Microsoft.AspNetCore.TestHost NuGet package:

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, 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/"));
    }
}

To reduce boilerplate, encapsulate the setup in a helper class:

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 requires a bit more setup since you need to configure additional 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?