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.
[Fact]
public async Task GetShippingAsync_WhenServiceFails_ThrowsException()
{
var handler = new FakeHttpMessageHandler(
(request, cancellationToken) =>
{
var response = new HttpResponseMessage(
HttpStatusCode.ServiceUnavailable);
return Task.FromResult(response);
});
using var httpClient = new HttpClient(handler)
{
BaseAddress =
new Uri("https://shipping.example.com/")
};
var service = new ShippingService(httpClient);
await Assert.ThrowsAsync<HttpRequestException>(
() => service.GetShippingAsync(123));
}This test verifies the behavior of the current service implementation. If you later change the service to handle failures and return a custom result, update the test to match the new contract.
You can use the same handler to simulate 404 Not Found, malformed JSON, or other responses.
For timeout tests, create a handler that waits until its cancellation token is triggered. Avoid using arbitrary long delays, which can make tests slow and unreliable.
Step 5: Mock External APIs in ASP.NET Core Integration Tests
Sometimes you need to test more than an individual service. You may want to send an HTTP request to your ASP.NET Core application and verify the complete request pipeline.
For that, use WebApplicationFactory from Microsoft.AspNetCore.Mvc.Testing with a test-specific service configuration.
The following example illustrates the approach:
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Xunit;
public class OrdersApiTests
{
[Fact]
public async Task GetOrder_ReturnsExpectedResponse()
{
await using var factory =
new WebApplicationFactory<Program>()
.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
services.RemoveAll<ShippingService>();
services.AddHttpClient<ShippingService>()
.ConfigurePrimaryHttpMessageHandler(
() =>
new FakeHttpMessageHandler(
(request, token) =>
Task.FromResult(
new HttpResponseMessage(
System.Net.HttpStatusCode.OK)
{
Content =
new StringContent(
"""
{
"status": "Shipped",
"trackingNumber": "TRACK123"
}
""",
Encoding.UTF8,
"application/json")
})));
});
});
var client = factory.CreateClient();
var response = await client.GetAsync("/api/orders/123");
response.EnsureSuccessStatusCode();
}
}This example assumes the application exposes /api/orders/123 and that the endpoint uses the registered ShippingService. The endpoint and expected response should be adjusted to match your actual application.
The test replaces the external HTTP handler while leaving the ASP.NET Core request pipeline available for testing.
For applications using top-level statements, you may need to expose the Program type to the test project with an appropriate declaration, such as public partial class Program { }.
Unit Tests vs. Integration Tests
Test type | What it checks | External API behavior |
|---|---|---|
Unit test | A service or individual component | Replaced with a fake handler |
ASP.NET Core integration test | Routing, middleware, dependency injection, and endpoint behavior | Replaced with a fake handler |
End-to-end test | Interaction with real deployed components | May use a real test service |
Contract test | Whether request and response formats match an agreed API contract | Uses contract definitions or a controlled provider |
Use unit tests for focused logic and integration tests when you need to verify how components work together.
Mocking the external HTTP handler does not prove that the third-party service follows its documented contract. If that matters, add contract tests or a small number of tests against the provider's supported test environment.
Common Mistakes to Avoid
Calling the Real API Accidentally
A test may still make a network request if the production HTTP client registration is not replaced correctly.
Verify that the fake handler receives the expected request and ensure that tests do not use real credentials or production endpoints.
Testing Only Successful Responses
External services can fail in several ways. Test unsuccessful status codes, malformed responses, timeouts, and cancellation where those cases matter.
Sharing Mutable Fake Responses Between Tests
Each request should receive a suitable response instance. Avoid reusing a response object that has already been disposed of or whose content has been consumed.
Mocking Too Much
A test that replaces every application component may pass even when the real dependency injection or HTTP configuration is incorrect.
Keep the production components you want to test and replace only the external dependency.
Ignoring Request Details
Checking only the response body may miss an incorrect URL, HTTP method, query parameter, or request header. Assert the request details that are important to the integration.
Best Practices for Production Applications
- Use
IHttpClientFactoryto manage HTTP client configuration and handler lifetimes. - Keep fake handlers in the test project rather than production code.
- Test successful responses, unsuccessful status codes, and cancellation behavior.
- Use deterministic test responses instead of relying on external network availability.
- Verify URLs, HTTP methods, and important request headers.
- Avoid real production credentials in automated tests.
- Test serialization and deserialization using realistic response payloads.
- Keep a small number of provider-level tests when actual integration compatibility matters.
- Ensure every test can run independently and does not depend on shared mutable state.
- Configure timeouts and cancellation in production clients so failed dependencies do not hold resources indefinitely.
Advantages and Disadvantages
Advantages
- Tests run without depending on external service availability.
- Responses can be controlled and repeated consistently.
- Error conditions are easier to reproduce.
- Tests generally run without external network delays.
- The same fake handler can support multiple test scenarios.
Disadvantages
- A fake response may not represent the actual provider's behavior.
- Mocking does not prove that production credentials, DNS, TLS, or network configuration work.
- Over-mocking can hide problems in dependency injection or HTTP configuration.
- More realistic integration tests may still be needed for important external dependencies.
Summary
Mocking external HTTP APIs in .NET integration tests helps you test your application without depending on a live external service.
By replacing HttpMessageHandler, you can control the response received by HttpClient and verify how your service handles successful requests, errors, and other conditions.
For unit tests, a custom handler provides a simple way to test individual services. For ASP.NET Core integration tests, WebApplicationFactory lets you replace the external dependency while testing the application's request pipeline.
Keep the mocks focused, test failure scenarios, and use separate contract or provider tests when you need confidence that the real external API behaves as expected.

Join the conversation! Your thoughts help the community grow.