Many .NET applications depend on external APIs to retrieve customer details, check payment status, send notifications, or fetch product information. These integrations work well when the external service is available, but they can make automated tests difficult to run reliably.
For example, a test that calls a real payment API might fail because the service is temporarily unavailable. It could also take longer to run or create unwanted transactions.
A better approach is to replace the external HTTP response with a controlled response during testing. This lets you test your application's behavior without depending on the actual external service.
In .NET, one practical way to do this is to mock HttpClient by providing a custom HttpMessageHandler. This article explains how to implement that approach, test an ASP.NET Core application, and avoid common mistakes in integration tests.
Why Mock External APIs in Tests?
Suppose your application calls a shipping service to retrieve delivery information. Your code must handle several possible responses:
- A successful response containing shipping details.
- A
404 Not Foundresponse when an order does not exist. - A
500 Internal Server Errorresponse from the external service. - A timeout or network failure.
- An unexpected or invalid response body.
Testing all these cases against the real service can be unreliable. You may not be able to reproduce a particular error whenever you need it.
Mocking allows you to control the response and verify how your application behaves under each condition.
It also keeps tests independent of external service availability, API credentials, rate limits, and changes in third-party test environments.
Understand How HttpClient Sends Requests
HttpClient uses an HttpMessageHandler to send HTTP requests and receive responses.
This design makes it possible to replace the actual network handler with a custom handler during testing.
The application continues using HttpClient, but the handler returns a response created by the test instead of sending a request over the network.
The important distinction is that you are testing your application's HTTP integration logic, not the external provider's actual implementation.
Step 1: Create a Service That Calls an External API
First, create a simple service that retrieves shipping information.
using System.Net.Http.Json;
public sealed class ShippingService
{
private readonly HttpClient _httpClient;
public ShippingService(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<ShippingInfo?> GetShippingAsync(
int orderId,
CancellationToken cancellationToken = default)
{
return await _httpClient.GetFromJsonAsync<ShippingInfo>(
$"api/orders/{orderId}/shipping",
cancellationToken);
}
}
public sealed class ShippingInfo
{
public string Status { get; set; } = string.Empty;
public string TrackingNumber { get; set; } = string.Empty;
}The service uses GetFromJsonAsync to send a GET request and deserialize the JSON response.
Passing a cancellation token allows the caller to cancel the request when necessary.
Register the service using IHttpClientFactory:
builder.Services.AddHttpClient<ShippingService>(client =>
{
client.BaseAddress =
new Uri("https://shipping.example.com/");
client.Timeout = TimeSpan.FromSeconds(10);
});The base address is an example configuration value. In a real application, load the external API address from configuration and validate it for the relevant environment.
Step 2: Create a Fake HttpMessageHandler
Now create a custom handler that returns a response without making a real network request.
using System.Net;
using System.Text;
public sealed class FakeHttpMessageHandler
: HttpMessageHandler
{
private readonly Func<
HttpRequestMessage,
CancellationToken,
Task<HttpResponseMessage>> _responseFactory;
public FakeHttpMessageHandler(
Func<HttpRequestMessage,
CancellationToken,
Task<HttpResponseMessage>> responseFactory)
{
_responseFactory = responseFactory;
}
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
return _responseFactory(
request,
cancellationToken);
}
}This handler accepts a function that decides what response to return.
That makes it easy to simulate successful responses, missing records, server errors, and other situations without changing the production service.
The handler itself does not validate the response. The test controls what it returns, allowing you to check whether the application handles the response correctly.
Step 3: Write a Test for a Successful Response
The following example uses xUnit to verify that ShippingService correctly reads a JSON response.
using System.Net;
using System.Net.Http;
using Xunit;
public class ShippingServiceTests
{
[Fact]
public async Task GetShippingAsync_ReturnsShippingDetails()
{
var handler = new FakeHttpMessageHandler(
(request, cancellationToken) =>
{
Assert.Equal(
HttpMethod.Get,
request.Method);
Assert.Equal(
"/api/orders/123/shipping",
request.RequestUri!.AbsolutePath);
var response = new HttpResponseMessage(
HttpStatusCode.OK)
{
Content = new StringContent(
"""
{
"status": "Shipped",
"trackingNumber": "TRACK123"
}
""",
Encoding.UTF8,
"application/json")
};
return Task.FromResult(response);
});
using var httpClient = new HttpClient(handler)
{
BaseAddress =
new Uri("https://shipping.example.com/")
};
var service = new ShippingService(httpClient);
var result = await service.GetShippingAsync(123);
Assert.NotNull(result);
Assert.Equal("Shipped", result.Status);
Assert.Equal("TRACK123", result.TrackingNumber);
}
}This test checks both the outgoing request and the returned data.
The fake handler confirms that the service uses the expected HTTP method and URL. It then returns a JSON response that the service deserializes into a ShippingInfo object.
Because the test uses a custom handler, it does not call the real shipping API.
The example assumes the test project references the application code and the xUnit packages.
Step 4: Test Error Responses
A successful request is only one part of the testing process. Your application should also handle failures from external services.
For example, GetFromJsonAsync throws an exception when the response has an unsuccessful HTTP status code. You can test that behavior by returning a 503 Service Unavailable response.
Join the conversation! Your thoughts help the community grow.