Integration Testing with ASP.NET Core
Write comprehensive integration tests for real-world scenarios.
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.
Related Articles
Unit Testing Best Practices
Write effective unit tests that catch bugs and enable refactoring.
Read More testingUnit and Integration Testing
Learn the differences between unit and integration testing and how to implement both in your application test strategy.
Read More testingUnit Testing with xUnit
Write effective unit tests with xUnit and assertion libraries.
Read More