Integration Testing a Web App
WebApplicationFactory<Program> starts your real app in memory so tests send real HTTP requests through the whole pipeline.
public class MyTests : IClassFixture<WebApplicationFactory<Program>>A unit test calls one class directly. That leaves a gap: routing, model binding, serialisation, authentication and configuration are all code you did not run. An integration test closes the gap by starting the whole application and sending it a real request.
WebApplicationFactory
WebApplicationFactory<TEntryPoint> lives in the Microsoft.AspNetCore.Mvc.Testing package. TEntryPoint is your application's Program class. Creating the factory runs your real Program.cs, and CreateClient() returns an HttpClient wired straight to it. No port is opened and no process is launched; the request goes through the same pipeline a browser's would, in memory, in the test process.
Because the factory is expensive to build, hand it to the test class with IClassFixture<WebApplicationFactory<Program>> so xUnit builds it once per class.
Setting it up
Two project-level details catch everyone. The test project must use the Web SDK, Sdk="Microsoft.NET.Sdk.Web", so the ASP.NET Core framework reference resolves. And top-level statements compile into an internal Program class, which the test project cannot see, so you add public partial class Program { } to the end of Program.cs.
Against the example below, curl http://localhost:5000/health on the running app returns status 200 with the body healthy and content type text/plain. The test asserts exactly that, without a running app.
These tests are slower than unit tests and they use real configuration, so point them at a test database and never at a live one.
Example
using System.Net;
using System.Net.Http;
using Microsoft.AspNetCore.Mvc.Testing;
using Xunit;
namespace Shop.Api.Tests;
// IClassFixture means xUnit builds the factory once for this whole class.
// Building it runs the real Program.cs and starts the app in memory.
public class HealthTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient _client;
public HealthTests(WebApplicationFactory<Program> factory)
{
// This client talks to the in-memory app, not to a network port.
_client = factory.CreateClient();
}
[Fact]
public void GetHealth_ReturnsOkAndHealthyBody()
{
// .Result waits for the reply. Stage 4 replaces it with await.
HttpResponseMessage response = _client.GetAsync("/health").Result;
string body = response.Content.ReadAsStringAsync().Result;
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("healthy", body);
}
}
// dotnet test reports: Failed: 0, Passed: 1, Skipped: 0, Total: 1When to use it
- Prove a new endpoint returns 201 and a Location header before any client depends on it.
- Catch a route or JSON property-name change that compiles fine but breaks callers.
- Check that an unauthenticated request to a protected endpoint really gets 401.
More examples
The application the test starts
The whole application, including the line that exists only for the tests. Run it and curl http://localhost:5000/health returns 200 with the body healthy; the test asserts the same two things with no server listening. Note the two declarations are one class: partial means the compiler joins the generated part and this part together, which is why adding public here is enough.
// ---- Shop.Api/Program.cs: the shortest app that serves /health ----
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
WebApplication app = builder.Build();
app.MapGet("/health", () => "healthy");
app.Run();
// Top-level statements compile into an internal class named Program.
// This empty partial declaration makes that same class public, so the
// test project can name it in WebApplicationFactory<Program>.
public partial class Program
{
}Setting up the test project
Do these four steps in this order the first time and the setup errors never appear. Skip the SDK edit and the build fails with missing ASP.NET Core types even though the package is installed, because the framework reference comes from the Web SDK rather than from the package. Keep the test project separate from the app project; it is not meant to ship.
dotnet new xunit -o Shop.Api.Tests
# WebApplicationFactory<T> lives in this package
dotnet add Shop.Api.Tests package Microsoft.AspNetCore.Mvc.Testing
# the tests need to see the app's Program class
dotnet add Shop.Api.Tests reference Shop.Api/Shop.Api.csproj
# finally, open Shop.Api.Tests/Shop.Api.Tests.csproj and change
# <Project Sdk="Microsoft.NET.Sdk">
# to
# <Project Sdk="Microsoft.NET.Sdk.Web">
dotnet testPosting JSON and checking the response headers
This is the test unit tests cannot write: it proves the JSON body was bound to CreateItem, the handler ran, and the right status and Location header came back. PostAsJsonAsync, an extension method from System.Net.Http.Json, serialises the record for you. The ?. on Location is there because the header is typed as a nullable Uri, so reading .ToString() on it directly would warn.
using System.Net;
using System.Net.Http;
using System.Net.Http.Json;
using Microsoft.AspNetCore.Mvc.Testing;
using Xunit;
namespace Shop.Api.Tests;
public record CreateItem(string Name);
public class ItemsTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient _client;
public ItemsTests(WebApplicationFactory<Program> factory)
{
// AllowAutoRedirect is on by default; turn it off to assert on a 301 or 302.
_client = factory.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
}
[Fact]
public void PostItem_ReturnsCreatedWithLocation()
{
HttpResponseMessage response =
_client.PostAsJsonAsync("/items", new CreateItem("bolt")).Result;
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
Assert.Equal("/items/1", response.Headers.Location?.ToString());
}
}
Discussion