Mocking an HttpClient using ASP.NET Core TestServer
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
:
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
:
<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
:
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:
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
:
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:
[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!