Isaac.

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

  1. Test full workflows: Create, read, update, delete
  2. Use real dependencies: When possible
  3. Clean up: Reset state between tests
  4. Avoid interdependence: Tests should run independently
  5. 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.