Mocking HTTP calls in ASP.NET Integration Tests

9 June 2025

NuGet and TeamCity

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,
        });
    }
}

📎 Source Code

👉 Check out the full repo on GitHub