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!