When working with services that call external APIs, you'll probably want to mock those calls in your tests. This process is straightforward when working with a single service. You typically mock the HttpHandler
in the HttpClient
passed to the service:
var handlerMock = new Mock<HttpMessageHandler>();
handlerMock
.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>()
)
.ReturnsAsync(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent("{\"message\":\"Hello World!\"}")
});
var httpClient = new HttpClient(handlerMock.Object)
{
BaseAddress = new Uri("https://api.example.com/")
};
How would you go about doing that in an Integration Testing scenario, where services get wired up by the framework?
ASP.NET has a nifty way of hosting your ASP.NET API in memory using the Microsoft.AspNetCore.Mvc.Testing package. It takes just a few lines of code to fire up an in-memory API, but mocking the external calls becomes tricky unless you want to replace every single service registration with a custom HttpClient
+ mocked handler combo. There's an easier way: you can register a custom HttpMessageHandlerBuilder
.
I've uploaded a sample app if you're just interested in source code: 👉 Source Code on GitHub
📦 Why Mock HTTP in Integration Tests?
While unit tests typically mock dependencies, integration tests validate the end-to-end behavior of your application’s components. But calling live external services in tests isn’t ideal because:
- It introduces flakiness if the external service is down or slow.
- It slows down your test suite.
- It can cost real money.
📄 Wiring things together
Create a custom HttpMessageHandlerBuilder
public class TestHttpMessageHandler : HttpMessageHandlerBuilder
{
private HttpMessageHandler _handler;
public TestHttpMessageHandler(HttpMessageHandler handler) =>
_handler = handler;
public override IList<DelegatingHandler> AdditionalHandlers { get; } = [];
public override string? Name { get; set; }
public override HttpMessageHandler PrimaryHandler
{
get => _handler;
set => _handler = value;
}
public override HttpMessageHandler Build() => _handler;
}
Register this in the dependency injection:
public class CustomWebApplicationFactory : WebApplicationFactory<Program>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
var httpMessageHandlerMock = new Mock<HttpMessageHandler>()
.CatchAllNonHandledRequests()
.SetupJsonPlaceHolder();
services.RemoveAll<HttpMessageHandlerBuilder>();
services.AddSingleton<HttpMessageHandlerBuilder>(new TestHttpMessageHandler(httpMessageHandlerMock.Object));
});
builder.UseEnvironment("Test");
}
}
Register your HTTP calls:
public static Mock<HttpMessageHandler> SetupJsonPlaceHolder(this Mock<HttpMessageHandler> handler)
{
handler
.SetupSendAsync(HttpMethod.Get, "https://jsonplaceholder.typicode.com/todosx")
.ReturnsHttpResponseAsync(HttpStatusCode.OK, "ResponsePayloads/todos.json");
return handler;
}
I've created a few convenience methods to set up HTTP mocks here
CatchAllNonHandledRequests()
acts as a catch all handler, throwing an exception in case you missed a mapping:
UNHANDLED REQUEST 'GET https://jsonplaceholder.typicode.com/todos', register a mock for it
at Todos.IntegrationTests.HttpMockExtensions...
Run the tests as normal, an example using xUnit:
public class GetTodosTests : IClassFixture<CustomWebApplicationFactory>
{
private readonly WebApplicationFactory<Program> _factory;
public GetTodosTests(CustomWebApplicationFactory factory) =>
_factory = factory;
[Fact]
public async Task Test1()
{
// Arrange
var client = _factory.CreateClient();
// Act
var todos = await client.GetFromJsonAsync<Todo[]>("/todos");
// Assert
todos!.Length.ShouldBe(9);
todos[0].ShouldBeEquivalentTo(new Todo
{
UserId = 1,
Id = 1,
Title = "delectus aut autem",
Completed = false,
});
}
}