Интеграционное тестирование C# с Docker: продвинутый уровень
Интеграционное тестирование — критически важный этап разработки на C#, позволяющий проверить взаимодействие компонентов системы с реальными зависимостями: базами данных, очередями сообщений, внешними API. Использование Docker для этой цели стало стандартом индустрии, но на продвинутом уровне возникают вопросы изоляции, производительности и параллельного выполнения. В этой статье мы разберем профессиональные техники интеграционного тестирования C# с Docker, используя библиотеку Testcontainers и продвинутые конфигурации.
1. Настройка Testcontainers для .NET: базовая инфраструктура
Библиотека Testcontainers для .NET позволяет программно управлять контейнерами Docker в тестовом окружении. В отличие от ручного запуска docker-compose, Testcontainers автоматически стартует и останавливает контейнеры для каждого тестового класса или модуля, гарантируя чистоту окружения.
Установите пакет через NuGet:
dotnet add package Testcontainers --version 3.9.0
dotnet add package Testcontainers.PostgreSql --version 3.9.0
dotnet add package Testcontainers.MsSql --version 3.9.0
Пример базового класса для интеграционных тестов с 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();
}
}
Теперь каждый тестовый класс может использовать этот фикстур для получения изолированного экземпляра базы данных.
2. Изоляция тестов и управление состоянием контейнеров
Продвинутый уровень требует тонкого контроля над изоляцией. Вместо одного контейнера на весь набор тестов, используйте один контейнер на один тестовый класс или даже на один тест. Это предотвращает «грязное» состояние базы данных между тестами.
Реализация через атрибут Collection в 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);
}
}
Для полной изоляции на уровне теста используйте ITestOutputHelper и создавайте контейнер внутри метода DisposeAsync:
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. Параллельный запуск тестов и управление ресурсами
Одна из главных проблем — параллельное выполнение тестов, которые используют Docker. По умолчанию Testcontainers создает уникальные сетевые порты, но можно столкнуться с нехваткой ресурсов CPU/RAM. Решения:
- Ограничение параллелизма — используйте атрибут
[Collection]для последовательного выполнения тестов, работающих с одной базой. - Reusable containers — Testcontainers поддерживает флаг
WithReuse(true), позволяющий переиспользовать контейнер между запусками (полезно для CI). - Resource quotas — задайте лимиты CPU и памяти через
WithResourceMapping().
Пример настройки с ограничением ресурсов:
var container = new PostgreSqlBuilder()
.WithImage("postgres:15")
.WithReuse(true)
.WithResourceMapping(new ResourceMapping()
.WithCpuLimit(0.5)
.WithMemoryLimit(512 * 1024 * 1024))
.Build();
Для CI/CD добавьте в docker-compose.yml лимиты глобально:
services:
postgres-test:
image: postgres:15
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M
4. Интеграция с CI/CD пайплайнами и продвинутая конфигурация
В реальных проектах важно, чтобы интеграционные тесты работали в CI-среде (GitHub Actions, GitLab CI, Azure DevOps). Несколько рекомендаций:
- Docker-in-Docker (DinD) — в контейнеризированных CI-агентах используйте DinD или монтируйте сокет Docker хоста.
- Health checks — дождитесь полной готовности контейнера перед тестами.
- Логирование — собирайте логи контейнеров для диагностики.
Пример health check в Testcontainers:
var container = new PostgreSqlBuilder()
.WithImage("postgres:15")
.WithWaitStrategy(Wait.ForUnixContainer()
.UntilPortIsAvailable(5432)
.AddCustomWaitStrategy(new PostgresHealthCheck()))
.Build();
Для CI используйте единый docker-compose.ci.yml:
version: '3.8'
services:
test-runner:
build: .
depends_on:
postgres:
condition: service_healthy
environment:
- ConnectionStrings__Default=Host=postgres;Database=testdb;Username=test;Password=test
postgres:
image: postgres:15
healthcheck:
test: ["CMD-SHELL", "pg_isready -U test"]
interval: 5s
timeout: 5s
retries: 5
Заключение
Интеграционное тестирование C# с Docker на продвинутом уровне требует не только настройки контейнеров, но и продуман