Integration Testing with ASP.NET Core
Write comprehensive integration tests for real-world scenarios.
By EMEPublished: February 20, 2025
testingintegration testsxunitaspnet core
A Simple Analogy
Integration tests are like rehearsals for a play. You run the full system, including real databases and services, to ensure everything works together before opening night.
Why Integration Tests?
- Real scenarios: Test actual dependencies
- Catch issues: Mocking hides integration problems
- Confidence: Verify complete workflows
- Regression: Prevent breaking changes
- Documentation: Tests show usage patterns
WebApplicationFactory
public class ApiWebApplicationFactory : WebApplicationFactory<Program>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
// Replace real dependencies with test doubles
var descriptor = services.FirstOrDefault(
d => d.ServiceType == typeof(IOrderService));
if (descriptor != null)
services.Remove(descriptor);
services.AddScoped<IOrderService, TestOrderService>();
});
}
}
public class OrderControllerTests : IClassFixture<ApiWebApplicationFactory>
{
private readonly HttpClient _client;
public OrderControllerTests(ApiWebApplicationFactory factory)
{
_client = factory.CreateClient();
}
[Fact]
public async Task GetOrder_WithValidId_ReturnsOk()
{
// Arrange
var orderId = "order-123";
// Act
var response = await _client.GetAsync($"/api/orders/{orderId}");
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
var content = await response.Content.ReadAsStringAsync();
var order = JsonSerializer.Deserialize<OrderDto>(content);
order.Should().NotBeNull();
}
}
In-Memory Database Testing
public class OrderRepositoryTests
{
private AppDbContext GetDbContext()
{
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
return new AppDbContext(options);
}
[Fact]
public async Task SaveOrder_WithValidOrder_Succeeds()
{
// Arrange
var context = GetDbContext();
var repository = new OrderRepository(context);
var order = new Order { Id = 1, CustomerId = "cust-1", Total = 100m };
// Act
await repository.SaveAsync(order);
await context.SaveChangesAsync();
// Assert
var saved = await context.Orders.FindAsync(1);
saved.Should().NotBeNull();
saved.Total.Should().Be(100m);
}
}
Container-Based Tests
public class PostgresIntegrationTests : IAsyncLifetime
{
private readonly PostgresContainer _postgres = new PostgresBuilder()
.WithImage("postgres:15")
.WithDatabase("testdb")
.WithUsername("test")
.WithPassword("test")
.Build();
public async Task InitializeAsync()
{
await _postgres.StartAsync();
}
public async Task DisposeAsync()
{
await _postgres.StopAsync();
}
[Fact]
public async Task SaveOrder_ToRealDatabase_Succeeds()
{
// Connect to real container
var connection = _postgres.GetConnectionString();
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseNpgsql(connection)
.Options;
using var context = new AppDbContext(options);
await context.Database.MigrateAsync();
// Test with real database
var order = new Order { CustomerId = "cust-1", Total = 100m };
context.Orders.Add(order);
await context.SaveChangesAsync();
var saved = await context.Orders.FindAsync(order.Id);
saved.Should().NotBeNull();
}
}
API Integration Tests
public class OrderApiTests : IClassFixture<ApiWebApplicationFactory>
{
private readonly HttpClient _client;
public OrderApiTests(ApiWebApplicationFactory factory)
{
_client = factory.CreateClient();
}
[Fact]
public async Task CreateOrder_WithValidPayload_Returns201()
{
// Arrange
var request = new CreateOrderRequest
{
CustomerId = "cust-123",
Items = new List<OrderItem>
{
new OrderItem { ProductId = "p1", Quantity = 2, Price = 29.99m }
}
};
var json = JsonSerializer.Serialize(request);
var content = new StringContent(json, Encoding.UTF8, "application/json");
// Act
var response = await _client.PostAsync("/api/orders", content);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.Created);
var responseContent = await response.Content.ReadAsStringAsync();
var dto = JsonSerializer.Deserialize<OrderDto>(responseContent);
dto.Id.Should().NotBeEmpty();
}
[Fact]
public async Task GetOrder_AfterCreate_ReturnsCreatedOrder()
{
// Create first
var createResponse = await _client.PostAsync("/api/orders", ...);
var created = await createResponse.Content.ReadAsAsync<OrderDto>();
// Then get
var getResponse = await _client.GetAsync($"/api/orders/{created.Id}");
var retrieved = await getResponse.Content.ReadAsAsync<OrderDto>();
retrieved.Id.Should().Be(created.Id);
}
}
Best Practices
- Test full workflows: Create, read, update, delete
- Use real dependencies: When possible
- Clean up: Reset state between tests
- Avoid interdependence: Tests should run independently
- Test error paths: Not just happy paths
Related Concepts
- Unit testing mocks and stubs
- Performance testing
- End-to-end testing
- Test automation best practices
Summary
Integration tests validate complete system behavior by testing real dependencies. Use WebApplicationFactory and in-memory databases for fast, reliable integration tests.