Integration Test Setup
Overview
Integration tests verify the full request pipeline:
- WebApplicationFactory - In-memory test server
- Testcontainers - Real PostgreSQL in Docker
- Respawn - Fast database cleanup between tests
- Authentication helpers - Test with different users/roles
Quick Reference
| Component | Purpose |
|---|---|
IntegrationTestWebAppFactory | Custom test server factory |
BaseIntegrationTest | Base class for all tests |
Respawner | Database cleanup utility |
TestAuthHandler | Fake authentication handler |
Test Project Structure
tests/
└── {name}.Api.IntegrationTests/
├── Infrastructure/
│ ├── IntegrationTestWebAppFactory.cs
│ ├── BaseIntegrationTest.cs
│ ├── TestAuthHandler.cs
│ └── FakeUserContext.cs
├── {Feature}/
│ ├── Create{Entity}Tests.cs
│ └── Get{Entity}Tests.cs
└── {name}.Api.IntegrationTests.csproj
Template: Test Project File
<!-- tests/{name}.Api.IntegrationTests/{name}.Api.IntegrationTests.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="Respawn" Version="6.1.0" />
<PackageReference Include="Testcontainers.PostgreSql" Version="3.6.0" />
<PackageReference Include="xunit" Version="2.6.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\{name}.api\{name}.api.csproj" />
<ProjectReference Include="..\..\src\{name}.infrastructure\{name}.infrastructure.csproj" />
</ItemGroup>
</Project>
Template: WebApplicationFactory
// tests/{name}.Api.IntegrationTests/Infrastructure/IntegrationTestWebAppFactory.cs
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Testcontainers.PostgreSql;
using {name}.infrastructure;
namespace {name}.Api.IntegrationTests.Infrastructure;
public class IntegrationTestWebAppFactory
: WebApplicationFactory<Program>, IAsyncLifetime
{
private readonly PostgreSqlContainer _dbContainer = new PostgreSqlBuilder()
.WithImage("postgres:15-alpine")
.WithDatabase("testdb")
.WithUsername("postgres")
.WithPassword("postgres")
.Build();
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureTestServices(services =>
{
// ═══════════════════════════════════════════════════════════════
// REPLACE DATABASE WITH TEST CONTAINER
// ═══════════════════════════════════════════════════════════════
services.RemoveAll(typeof(DbContextOptions<ApplicationDbContext>));
services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseNpgsql(_dbContainer.GetConnectionString());
});
// ═══════════════════════════════════════════════════════════════
// REPLACE AUTHENTICATION WITH TEST HANDLER
// ═══════════════════════════════════════════════════════════════
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = TestAuthHandler.SchemeName;
options.DefaultChallengeScheme = TestAuthHandler.SchemeName;
})
.AddScheme<TestAuthSchemeOptions, TestAuthHandler>(
TestAuthHandler.SchemeName,
options => { });
// ═══════════════════════════════════════════════════════════════
// REPLACE EXTERNAL SERVICES WITH FAKES
// ═══════════════════════════════════════════════════════════════
// services.RemoveAll<IEmailService>();
// services.AddSingleton<IEmailService, FakeEmailService>();
});
builder.UseEnvironment("Testing");
}
public async Task InitializeAsync()
{
await _dbContainer.StartAsync();
// Apply migrations
using var scope = Services.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
await dbContext.Database.MigrateAsync();
}
public new async Task DisposeAsync()
{
await _dbContainer.StopAsync();
}
}
Template: Test Authentication Handler
// tests/{name}.Api.IntegrationTests/Infrastructure/TestAuthHandler.cs
using System.Security.Claims;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace {name}.Api.IntegrationTests.Infrastructure;
public class TestAuthSchemeOptions : AuthenticationSchemeOptions
{
public Guid? UserId { get; set; }
public string? Email { get; set; }
public string[]? Roles { get; set; }
public string[]? Permissions { get; set; }
}
public class TestAuthHandler : AuthenticationHandler<TestAuthSchemeOptions>
{
public const string SchemeName = "TestScheme";
public const string TestUserIdHeader = "X-Test-User-Id";
public const string TestUserEmailHeader = "X-Test-User-Email";
public const string TestUserRolesHeader = "X-Test-User-Roles";
public const string TestUserPermissionsHeader = "X-Test-User-Permissions";
public TestAuthHandler(
IOptionsMonitor<TestAuthSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder)
: base(options, logger, encoder)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
// Check for test headers
if (!Request.Headers.TryGetValue(TestUserIdHeader, out var userIdHeader))
{
return Task.FromResult(AuthenticateResult.NoResult());
}
if (!Guid.TryParse(userIdHeader, out var userId))
{
return Task.FromResult(AuthenticateResult.Fail("Invalid user ID"));
}
var claims = new List<Claim>
{
new(ClaimTypes.NameIdentifier, userId.ToString()),
new("sub", userId.ToString())
};
// Add email
if (Request.Headers.TryGetValue(TestUserEmailHeader, out var emailHeader))
{
claims.Add(new Claim(ClaimTypes.Email, emailHeader.ToString()));
claims.Add(new Claim("email", emailHeader.ToString()));
}
// Add roles
if (Request.Headers.TryGetValue(TestUserRolesHeader, out var rolesHeader))
{
foreach (var role in rolesHeader.ToString().Split(','))
{
claims.Add(new Claim(ClaimTypes.Role, role.Trim()));
}
}
// Add permissions
if (Request.Headers.TryGetValue(TestUserPermissionsHeader, out var permissionsHeader))
{
foreach (var permission in permissionsHeader.ToString().Split(','))
{
claims.Add(new Claim("permission", permission.Trim()));
}
}
var identity = new ClaimsIdentity(claims, SchemeName);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(princi