Applications often depend on external HTTP APIs for payment processing, email delivery, customer information, shipping calculations, and other services. These integrations work well when the external service is available, but they can make automated tests slow, unreliable, and difficult to repeat.
For example, an integration test that calls a real payment API may fail because of a network timeout, an API rate limit, expired credentials, or a temporary service outage. It may also create real transactions or consume paid API credits.
A better approach is to test your application's HTTP integration without depending on the actual external service. In .NET, you can do this by configuring HttpClient to use a custom HttpMessageHandler that returns controlled responses.
This article explains how to mock external HTTP APIs in .NET integration tests, test successful and failed responses, and avoid common testing mistakes.
Why Mock External HTTP APIs?
Consider an ASP.NET Core application that retrieves customer information from an external service.
The application might send an HTTP request to an endpoint such as /customers/123. Your code needs to handle the response correctly, including successful responses, missing records, server errors, and timeouts.
Calling the real service for every test creates several problems:
Tests depend on network connectivity and the external service's availability.
Test results can change when the external service changes its data.
Requests may require real credentials or paid API access.
Tests can become slower because of network latency.
Failures become harder to diagnose because the problem may be outside your application.
Mocking the HTTP boundary allows tests to control the response and verify how the application behaves.
It is important to understand what this approach tests. A mocked HTTP handler can test your request construction, response handling, serialization, error handling, and application behavior. It does not prove that the external service is available or that its real API contract has not changed. Those concerns may require a small number of separate contract or sandbox tests.
Create a Service That Uses HttpClient
Before writing the test, create a service that communicates with the external API.
Suppose the external service returns customer details in JSON.
public sealed class Customer
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
}Create a service that retrieves a customer by ID.
using System.Net;
using System.Net.Http.Json;
public sealed class CustomerApiClient
{
private readonly HttpClient _httpClient;
public CustomerApiClient(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<Customer?> GetCustomerAsync(
int customerId,
CancellationToken cancellationToken = default)
{
using var response = await _httpClient.GetAsync(
$"customers/{customerId}",
cancellationToken);
if (response.StatusCode == HttpStatusCode.NotFound)
{
return null;
}
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<Customer>(
cancellationToken: cancellationToken);
}
}The service receives HttpClient through its constructor. This makes it easy to configure a real HTTP client in the application and a test-specific client during testing.
The service also distinguishes between a missing customer and an unsuccessful HTTP response. A 404 Not Found returns null, while other unsuccessful status codes cause EnsureSuccessStatusCode() to throw an exception.
Keeping this behavior in a dedicated client makes it easier to test and reuse the integration.
Mock HttpClient with a Custom HttpMessageHandler
HttpClient delegates its HTTP operations to an HttpMessageHandler. By supplying a custom handler, a test can intercept requests and return predefined responses without sending anything over the network.
Create a test handler:
using System.Net;
using System.Net.Http;
public sealed class FakeHttpMessageHandler : HttpMessageHandler
{
private readonly Func<
HttpRequestMessage,
CancellationToken,
Task<HttpResponseMessage>> _sendAsync;
public FakeHttpMessageHandler(
Func<
HttpRequestMessage,
CancellationToken,
Task<HttpResponseMessage>> sendAsync)
{
_sendAsync = sendAsync;
}
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
return _sendAsync(request, cancellationToken);
}
}This handler accepts a delegate that determines how each request should be handled. A test can inspect the request and return any response required for the scenario.
The handler is intentionally small. It does not need to implement DNS resolution, sockets, TLS, or actual network communication.
Write a Test for a Successful Response
The following example uses xUnit to verify that the client retrieves and deserializes a customer response.
using System.Net;
using System.Text;
using Xunit;
public sealed class CustomerApiClientTests
{
[Fact]
public async Task GetCustomerAsync_ReturnsCustomer_WhenRequestSucceeds()
{
var handler = new FakeHttpMessageHandler(
(request, cancellationToken) =>
{
Assert.Equal(HttpMethod.Get, request.Method);
Assert.Equal(
"/customers/123",
request.RequestUri!.AbsolutePath);
var response = new HttpResponseMessage(
HttpStatusCode.OK)
{
Content = new StringContent(
"""
{
"id": 123,
"name": "Aarav Patel",
"email": "[email protected]"
}
""",
Encoding.UTF8,
"application/json")
};
return Task.FromResult(response);
});
using var httpClient = new HttpClient(handler)
{
BaseAddress = new Uri("https://api.example.test/")
};
var customerApiClient = new CustomerApiClient(httpClient);
var customer = await customerApiClient.GetCustomerAsync(123);
Assert.NotNull(customer);
Assert.Equal(123, customer.Id);
Assert.Equal("Aarav Patel", customer.Name);
Assert.Equal("[email protected]", customer.Email);
}
}
Join the conversation! Your thoughts help the community grow.