Integration tests catch routing, binding, DI and serialization bugs that unit tests miss.
Microsoft.AspNetCore.Mvc.Testing provides WebApplicationFactory<TEntryPoint>, which boots your real app (its Program) on an in-memory TestServer. factory.CreateClient() returns an HttpClient wired to it: requests go through the full middleware pipeline, routing, binding and serialization, without opening a network port.
Setup:
- Create an xUnit project, add
Microsoft.AspNetCore.Mvc.Testing, and reference the web project. - The test project needs access to
Program. If the compiler can't see it, addpublic partial class Program { }at the end of the app'sProgram.cs. - Use
IClassFixture<WebApplicationFactory<Program>>to share one app instance across tests in a class.
Customize the app for tests with WithWebHostBuilder:
builder.UseEnvironment("Testing")andUseSetting("Key", "value")for configuration.ConfigureTestServices(services => ...)to replace real dependencies (external APIs, clocks, email senders) with fakes. Remove the real registration or rely on the last registration winning.
For databases, prefer a real engine in a container (Testcontainers) or SQLite over the EF in-memory provider, which doesn't behave like a relational database.
Use System.Net.Http.Json helpers: GetFromJsonAsync<T>, PostAsJsonAsync, ReadFromJsonAsync<T>. Assert on status codes and response bodies, as an API client would.
Example
// Api.Tests/TodoApiTests.cs (xUnit + Microsoft.AspNetCore.Mvc.Testing)
using System.Net;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
public class TodoApiTests(WebApplicationFactory<Program> factory) : IClassFixture<WebApplicationFactory<Program>>
{
[Fact]
public async Task Get_unknown_todo_returns_404()
{
var response = await factory.CreateClient().GetAsync("/todos/999");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact] // IClock and FixedClock are defined in the app under test
public async Task Greeting_uses_fake_clock()
{
var client = factory.WithWebHostBuilder(b => b.ConfigureTestServices(s =>
s.AddSingleton<IClock>(new FixedClock(new DateTime(2026, 1, 1, 9, 0, 0))))).CreateClient();
var text = await client.GetStringAsync("/hello/Ada");
Assert.StartsWith("Good morning", text);
}
}Remember
- WebApplicationFactory<Program> hosts the real app in memory
- CreateClient gives an HttpClient through the full pipeline
- ConfigureTestServices swaps real dependencies for fakes
- Prefer a real database engine over the EF in-memory provider
Watch out for
- Testing only handlers in isolation and missing binding, routing or serialization bugs.