Integration Testing ASP.NET Core with WebApplicationFactory
Unit tests are great for isolated logic, but they can't tell you whether your middleware pipeline, model binding, routing, and dependency injection all work together. That's where WebApplicationFactory comes in — it spins up your entire ASP.NET Core application in-memory, letting you make real HTTP requests without touching a network socket.
Getting Started
Add the Microsoft.AspNetCore.Mvc.Testing package to your test project:
dotnet add package Microsoft.AspNetCore.Mvc.Testing
Your test project also needs a reference to your web application project. If you're using the minimal API style with top-level statements, make sure your web project has this in its .csproj:
<ItemGroup>
<InternalsVisibleTo Include="YourApp.Tests" />
</ItemGroup>
Or add a partial Program class at the bottom of Program.cs:
public partial class Program { }
Your First Integration Test
public class WeatherApiTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient _client;
public WeatherApiTests(WebApplicationFactory<Program> factory)
{
_client = factory.CreateClient();
}
[Fact]
public async Task GetWeather_ReturnsSuccessStatusCode()
{
var response = await _client.GetAsync("/weatherforecast");
response.EnsureSuccessStatusCode();
}
[Fact]
public async Task GetWeather_ReturnsJsonContent()
{
var response = await _client.GetAsync("/weatherforecast");
var content = await response.Content.ReadAsStringAsync();
var forecasts = JsonSerializer.Deserialize<WeatherForecast[]>(content,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
Assert.NotNull(forecasts);
Assert.NotEmpty(forecasts);
}
}
By implementing IClassFixture<WebApplicationFactory<Program>>, xUnit shares the factory (and therefore the test server) across all tests in the class. The application starts once, not per test.
Customising the Test Server
The real power comes from WebApplicationFactory<T>.WithWebHostBuilder, which lets you replace services, change configuration, and modify the application for testing.
public class CustomWebApplicationFactory : WebApplicationFactory<Program>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
// Remove the real database context
var descriptor = services.SingleOrDefault(
d => d.ServiceType == typeof(DbContextOptions<AppDbContext>));
if (descriptor != null)
services.Remove(descriptor);
// Add an in-memory database for testing
services.AddDbContext<AppDbContext>(options =>
{
options.UseInMemoryDatabase("TestDb");
});
});
builder.ConfigureAppConfiguration((context, config) =>
{
config.AddInMemoryCollection(new Dictionary<string, string>
{
["Feature:EnableNewDashboard"] = "true",
["Logging:LogLevel:Default"] = "Warning"
});
});
}
}
Use it in your tests:
public class OrderApiTests : IClassFixture<CustomWebApplicationFactory>
{
private readonly HttpClient _client;
private readonly CustomWebApplicationFactory _factory;
public OrderApiTests(CustomWebApplicationFactory factory)
{
_factory = factory;
_client = factory.CreateClient();
}
[Fact]
public async Task CreateOrder_ReturnsCreated()
{
var order = new CreateOrderRequest("Widget", 5);
var content = JsonContent.Create(order);
var response = await _client.PostAsync("/api/orders", content);
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
}
}
Testing Authentication
Most APIs require authentication. You can bypass this in tests by adding a test authentication handler:
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public TestAuthHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder)
: base(options, logger, encoder) { }
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var claims = new[]
{
new Claim(ClaimTypes.Name, "testuser"),
new Claim(ClaimTypes.Role, "Admin")
};
var identity = new ClaimsIdentity(claims, "Test");
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, "TestScheme");
return Task.FromResult(AuthenticateResult.Success(ticket));
}
}
Register it in your custom factory:
builder.ConfigureServices(services =>
{
services.AddAuthentication("TestScheme")
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>("TestScheme", _ => { });
});
Seeding Test Data
When you need specific data for a test, use a scope to access services directly:
[Fact]
public async Task GetOrder_ExistingOrder_ReturnsOrder()
{
// Arrange - seed the database
using (var scope = _factory.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
db.Orders.Add(new Order { Id = 42, Product = "Widget", Quantity = 3 });
await db.SaveChangesAsync();
}
// Act
var response = await _client.GetAsync("/api/orders/42");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
Performance Considerations
WebApplicationFactory starts your real application, so first test execution involves the full startup cost. After that, the server stays running. A few tips:
- Use
IClassFixtureorICollectionFixtureto share the factory across tests. - Avoid creating a new
WebApplicationFactoryper test — this is the most common performance mistake. - If you're using a real database (via Testcontainers or similar), pair it with Respawn to reset state between tests rather than recreating the database.
What Gets Tested
This approach exercises your entire request pipeline: routing, model binding, validation, filters, middleware, serialisation, and the DI container. It's remarkably close to a real deployment — the only thing missing is the actual network layer.
For most ASP.NET Core applications, a combination of unit tests for business logic and WebApplicationFactory-based integration tests for API behaviour provides excellent coverage with fast execution times.