Integration Testing C# with Docker: Advanced Level
Integration testing is a critically important stage of C# development, allowing you to verify the interaction of system components with real dependencies: databases, message queues, external APIs. Using Docker for this purpose has become an industry standard, but at an advanced level, questions of isolation, performance, and parallel execution arise. In this article, we will cover professional techniques for integration testing C# with Docker, using the Testcontainers library and advanced configurations.
1. Setting up Testcontainers for .NET: Basic Infrastructure
The Testcontainers library for .NET allows you to programmatically manage Docker containers in a test environment. Unlike manually running docker-compose, Testcontainers automatically starts and stops containers for each test class or module, guaranteeing a clean environment.
Install the package via NuGet:
dotnet add package Testcontainers --version 3.9.0dotnet add package Testcontainers.PostgreSql --version 3.9.0dotnet add package Testcontainers.MsSql --version 3.9.0Example of a base class for integration tests with PostgreSQL:
using Testcontainers.PostgreSql;using Xunit;
public class DatabaseFixture : IAsyncLifetime{ private readonly PostgreSqlContainer _container = new PostgreSqlBuilder() .WithImage("postgres:15-alpine") .WithDatabase("testdb") .WithUsername("testuser") .WithPassword("testpass") .Build();
public string ConnectionString => _container.GetConnectionString();
public async Task InitializeAsync() { await _container.StartAsync(); }
public async Task DisposeAsync() { await _container.StopAsync(); }}Now each test class can use this fixture to obtain an isolated database instance.
2. Test Isolation and Container State Management
The advanced level requires fine-grained control over isolation. Instead of one container for the entire test suite, use one container per test class or even per test. This prevents "dirty" database state between tests.
Implementation using the Collection attribute in xUnit:
[CollectionDefinition("Database collection")]public class DatabaseCollection : ICollectionFixture<DatabaseFixture>{}
[Collection("Database collection")]public class UserRepositoryTests{ private readonly DatabaseFixture _fixture;
public UserRepositoryTests(DatabaseFixture fixture) { _fixture = fixture; }
[Fact] public async Task CreateUser_ShouldPersistUser() { // Arrange await using var context = new AppDbContext(_fixture.ConnectionString); await context.Database.EnsureCreatedAsync(); var repo = new UserRepository(context);
// Act var user = new User { Name = "Test User" }; await repo.CreateAsync(user);
// Assert var savedUser = await context.Users.FindAsync(user.Id); Assert.NotNull(savedUser); }}For full isolation at the test level, use ITestOutputHelper and create the container inside the DisposeAsync method:
public class IsolatedTest : IAsyncLifetime{ private PostgreSqlContainer _container;
public async Task InitializeAsync() { _container = new PostgreSqlBuilder().Build(); await _container.StartAsync(); }
public async Task DisposeAsync() { await _container.DisposeAsync(); }}3. Parallel Test Execution and Resource Management
One of the main