The HttpClientMock class allows you to mock an HttpClient instance. You can configure the mock to return a specific response for a given request. You can also forward the request to an upstream server.
It uses ASP.NET Core with the TestServer to handle the requests, so you can easily define the responses using the same syntax as you would in an ASP.NET Core application.
You can use the MapGet, MapPost, and so on, methods to define the response for a given request. You can also use the ForwardUnknownRequestsToUpstream method to forward the request to upstream server.
The url provided to the Map methods can be relative or absolute. It can also contains a query string.
await using var mock = new HttpClientMock();
mock.MapGet("/", () => Results.Ok("test"));
mock.MapGet("/todos/{id}", (int id) => Results.Ok("test"));
mock.MapGet("/todos?search=abc", () => Results.Ok("test"));
mock.MapGet("/todos", (string search) => Results.Ok("test"));
mock.MapGet("https://example.com/path?search=text", () => Results.Ok("test"));
// Return raw content
mock.MapGet("/json", () => Results.Extensions.RawJson("""{"id": 1}"""));
mock.MapGet("/xml", () => Results.Extensions.RawXml("""<root></root>"""));
// Forward the request to the upstream server
mock.MapGet("/upstream", () => Results.Extensions.ForwardToUpstream());
// Forward any request that doesn't match a configure route to the upstream server
mock.ForwardUnknownRequestsToUpstream();The RequestCounter class allows you to count the number of requests per endpoint. You can use it to test if a specific endpoint has been called.
A request is counted before its handler runs, so Get() includes the request being handled, and a request whose handler throws is still counted.
mock.MapGet("/counter", (RequestCounter counter) => counter.Get());There are multiple ways to use the mock depending on your needs:
-
Get an
HttpClientinstance from the mockusing var httpClient = mock.CreateHttpClient();
-
Get an
HttpMessageHandlerinstance from the mockusing var httpMessageHandler = mock.CreateHttpMessageHandler();
-
Register the mock in the service collection (Dependency Injection)
var services = new ServiceCollection(); services.AddHttpClient(); services.AddHttpClient<SampleClient>(); // Mock both HttpClient services.AddHttpClientMock(builder => builder .AddHttpClientMock(mock) .AddHttpClientMock<SampleClient>(mock)); // Alternative way to register the mock services.AddHttpClient<SampleClient>() .ConfigurePrimaryHttpMessageHandler(() => mock.CreateHttpMessageHandler());
By default, an
HttpClientfor which no mock is registered sends real HTTP requests. UseThrowOnUnknownHttpClientto fail instead, so a client you forgot to mock cannot silently reach the network.services.AddHttpClientMock(builder => builder .AddHttpClientMock(mock) .ThrowOnUnknownHttpClient());
When forwarding requests to an upstream server, you can use resiliency policies to handle transient failures. You can use the Microsoft.Extensions.Http.Resilience package.
await using var mock = new HttpClientMock(configureLogging: null, configureServices: services =>
{
services.ConfigureHttpClientDefaults(services => services.AddStandardResilienceHandler());
});You can forward logs to the xUnit ITestOutputHelper. This can be useful to debug issues with the mock.
- Add the
Meziantou.Extensions.Logging.Xunit.v3package to your project -
using var loggerProvider = new XUnitLoggerProvider(testOutputHelper); await using var mock = new HttpClientMock(loggerProvider);
If you need more controls about logging, you can use the configureLogging parameter of the constructor. Note that a bare
null is ambiguous between the constructor overloads, so use a named argument to select one
(new HttpClientMock(configureLogging: null, configureServices: ...)).
using var loggerProvider = new XUnitLoggerProvider(testOutputHelper);
await using var mock = new HttpClientMock(logs =>
{
logs.AddProvider(loggerProvider);
logs.SetMinimumLevel(LogLevel.Trace);
});The mock does not read the configuration of the host application: appsettings.json, the ASPNETCORE_*/DOTNET_*
environment variables and the command line are ignored, so a mock behaves the same on any machine. Use the
configureServices parameter to add the configuration sources you need.
await using var mock = new HttpClientMock(configureLogging: null, configureServices: services =>
{
services.AddSingleton<IConfiguration>(new ConfigurationBuilder().AddInMemoryCollection(...).Build());
});