Backend Systemsbeginner
WebApplicationFactory — Real HTTP Tests Without a Browser
Write integration tests in ASP.NET Core using WebApplicationFactory. Test real HTTP endpoints, middleware, authentication, and database interactions.
Asma HafeezApril 17, 20263 min read
dotnettestingintegration-testswebappfactoryaspnet-core
Integration Tests with WebApplicationFactory
Integration tests spin up your actual ASP.NET Core app in-memory and make real HTTP requests. They catch bugs that unit tests miss: routing, middleware, serialization, authentication.
Setup
Bash
dotnet add package Microsoft.AspNetCore.Mvc.Testing
dotnet add package FluentAssertionsXML
<!-- MyApp.Tests.csproj -->
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.*" />
<ProjectReference Include="..\MyApp\MyApp.csproj" />
</ItemGroup>Basic Integration Test
C#
public class ProductsApiTests(WebApplicationFactory<Program> factory)
: IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient _client = factory.CreateClient();
[Fact]
public async Task GetProducts_ReturnsOk()
{
var response = await _client.GetAsync("/products");
response.StatusCode.Should().Be(HttpStatusCode.OK);
}
[Fact]
public async Task GetProduct_NotFound_Returns404()
{
var response = await _client.GetAsync("/products/99999");
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
}
[Fact]
public async Task CreateProduct_ValidRequest_Returns201()
{
var body = JsonContent.Create(new { name = "Widget", price = 9.99 });
var response = await _client.PostAsync("/products", body);
response.StatusCode.Should().Be(HttpStatusCode.Created);
var product = await response.Content.ReadFromJsonAsync<Product>();
product!.Name.Should().Be("Widget");
}
}Custom WebApplicationFactory with In-Memory Database
C#
public class TestWebFactory : WebApplicationFactory<Program>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureTestServices(services =>
{
// Remove real DB
var descriptor = services.SingleOrDefault(
d => d.ServiceType == typeof(DbContextOptions<AppDbContext>));
if (descriptor is not null) services.Remove(descriptor);
// Add in-memory DB
services.AddDbContext<AppDbContext>(options =>
options.UseInMemoryDatabase("TestDb_" + Guid.NewGuid()));
// Seed test data
var sp = services.BuildServiceProvider();
using var scope = sp.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
db.Database.EnsureCreated();
SeedTestData(db);
});
}
private static void SeedTestData(AppDbContext db)
{
db.Products.AddRange(
new Product { Id = 1, Name = "Widget", Price = 9.99m },
new Product { Id = 2, Name = "Gadget", Price = 49.99m },
new Product { Id = 3, Name = "Doohickey", Price = 19.99m }
);
db.SaveChanges();
}
}
// Use the custom factory
public class ProductTests(TestWebFactory factory) : IClassFixture<TestWebFactory>
{
private readonly HttpClient _client = factory.CreateClient();
[Fact]
public async Task GetAll_ReturnsSeedData()
{
var products = await _client.GetFromJsonAsync<List<Product>>("/products");
products!.Should().HaveCount(3);
}
}Testing with Authentication
C#
public class TestWebFactory : WebApplicationFactory<Program>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureTestServices(services =>
{
// Replace JWT auth with a test scheme
services.AddAuthentication("Test")
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>("Test", _ => { });
});
}
}
// Test auth handler — always authenticates as a specific user
public class TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger, UrlEncoder encoder) : AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder)
{
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, "test-user-id"),
new Claim(ClaimTypes.Email, "test@example.com"),
new Claim(ClaimTypes.Role, "Admin"),
};
var identity = new ClaimsIdentity(claims, "Test");
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, "Test");
return Task.FromResult(AuthenticateResult.Success(ticket));
}
}
// Test authenticated endpoints
[Fact]
public async Task AdminEndpoint_WithTestAuth_Succeeds()
{
// The TestAuthHandler automatically provides Admin credentials
var response = await _client.GetAsync("/admin/users");
response.StatusCode.Should().Be(HttpStatusCode.OK);
}Testing Request/Response Serialization
C#
[Fact]
public async Task CreateProduct_SerializationRoundTrip()
{
// Arrange
var request = new CreateProductRequest("Test Widget", 15.99m, "Electronics");
// Act
var postResponse = await _client.PostAsJsonAsync("/products", request);
postResponse.EnsureSuccessStatusCode();
var created = await postResponse.Content.ReadFromJsonAsync<ProductResponse>();
created.Should().NotBeNull();
created!.Id.Should().BeGreaterThan(0);
created.Name.Should().Be("Test Widget");
created.Price.Should().Be(15.99m);
// Verify it's retrievable
var getResponse = await _client.GetFromJsonAsync<ProductResponse>($"/products/{created.Id}");
getResponse!.Name.Should().Be("Test Widget");
}Key Takeaways
WebApplicationFactory<Program>spins up your real app in-memory — tests run without a server- Override
ConfigureTestServicesto replace real dependencies (DB, email, payment providers) - Use in-memory database for fast tests without requiring a real database
- Custom authentication handlers let you test secured endpoints without real tokens
- Integration tests are slower than unit tests — run them in CI, not on every keystroke
Enjoyed this article?
Explore the Backend Systems learning path for more.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.