Интеграционное тестирование C# с Docker: продвинутый уровень
Интеграционное тестирование — ключевой этап обеспечения качества в современных .NET-приложениях. Использование Docker позволяет создавать изолированные, воспроизводимые среды, максимально приближенные к продакшену. В этой статье мы рассмотрим продвинутые техники интеграционного тестирования C# с Docker: от настройки Testcontainers до организации CI/CD-пайплайнов.
Материал ориентирован на разработчиков, уже знакомых с основами модульного тестирования и базовыми концепциями Docker. Мы углубимся в управление жизненным циклом контейнеров, работу с несколькими сервисами, отладку и оптимизацию времени выполнения тестов.
1. Настройка Testcontainers для .NET
Testcontainers — это библиотека, которая управляет контейнерами Docker из кода тестов. Она автоматически создает, запускает и останавливает контейнеры, гарантируя изоляцию каждого теста.
Для начала установите пакет в ваш тестовый проект:
dotnet add package Testcontainers
Базовый пример — тестирование репозитория с PostgreSQL:
using Testcontainers.PostgreSql;
using Xunit;
public class UserRepositoryTests : IAsyncLifetime
{
private readonly PostgreSqlContainer _postgresContainer;
public UserRepositoryTests()
{
// Настраиваем контейнер с PostgreSQL
_postgresContainer = new PostgreSqlBuilder()
.WithImage("postgres:15")
.WithDatabase("testdb")
.WithUsername("test")
.WithPassword("test123")
.WithCleanUp(true)
.Build();
}
public async Task InitializeAsync()
{
// Запускаем контейнер перед всеми тестами
await _postgresContainer.StartAsync();
}
public async Task DisposeAsync()
{
// Останавливаем и удаляем контейнер
await _postgresContainer.DisposeAsync();
}
[Fact]
public async Task AddUser_ShouldCreateUser()
{
// Получаем строку подключения к контейнеру
var connectionString = _postgresContainer.GetConnectionString();
var repository = new UserRepository(connectionString);
await repository.AddUserAsync(new User { Name = "Test User" });
var user = await repository.GetUserByNameAsync("Test User");
Assert.NotNull(user);
Assert.Equal("Test User", user.Name);
}
}
Важно: используйте IAsyncLifetime для управления контейнером. Это гарантирует, что каждый тестовый класс получит свежий экземпляр базы данных.
2. Многоконтейнерная архитектура с Docker Compose
Реальные приложения зависят от нескольких сервисов: базы данных, очереди сообщений, кэша. Testcontainers поддерживает Docker Compose, что позволяет запускать целую инфраструктуру.
Установите дополнительный пакет:
dotnet add package Testcontainers.Compose
Пример файла docker-compose.test.yml:
version: '3.8'
services:
postgres:
image: postgres:15
environment:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: testdb
redis:
image: redis:7-alpine
Использование в тесте:
using Testcontainers.Compose;
public class OrderServiceTests
{
private readonly DockerComposeContainer _compose;
public OrderServiceTests()
{
_compose = new DockerComposeContainer()
.WithComposeFilePath("docker-compose.test.yml")
.WithEnvironment("ASPNETCORE_ENVIRONMENT", "Testing")
.Build();
}
[Fact]
public async Task CreateOrder_ShouldSaveToDbAndCache()
{
await _compose.StartAsync();
var dbConnection = _compose.GetConnectionString("postgres", 5432);
var redisConnection = _compose.GetConnectionString("redis", 6379);
var service = new OrderService(dbConnection, redisConnection);
var result = await service.CreateOrderAsync(new Order { Amount = 100 });
Assert.True(result.IsSuccess);
await _compose.StopAsync();
}
}
Совет: для ускорения тестов используйте WithCleanUp(true) или явно вызывайте StopAsync() в Dispose.
3. Изоляция тестов и параллельное выполнение
Одна из главных проблем интеграционных тестов — конфликт данных между параллельными запусками. Решения:
- Уникальные базы данных: создавайте отдельную схему или базу для каждого тестового класса.
- Использование фикстур: Testcontainers позволяет создавать контейнеры на уровне класса или коллекции.
- Параллелизация: используйте
CollectionDefinitionв xUnit для группировки тестов, которые не должны выполняться одновременно.
Пример настройки параллельного выполнения с изоляцией:
[CollectionDefinition("Database", DisableParallelization = false)]
public class DatabaseCollection : ICollectionFixture<DatabaseFixture> { }
public class DatabaseFixture : IAsyncLifetime
{
private readonly PostgreSqlContainer _container;
public DatabaseFixture()
{
_container = new PostgreSqlBuilder()
.WithDatabase($"testdb_{Guid.NewGuid():N}")
.Build();
}
public async Task InitializeAsync() => await _container.StartAsync();
public async Task DisposeAsync() => await _container.DisposeAsync();
public string ConnectionString => _container.GetConnectionString();
}
Это гарантирует, что каждый тестовый класс получит уникальную базу данных, а параллельные тесты не будут влиять друг на друга.
4. Интеграция в CI/CD и отладка
Для успешного запуска в CI-среде (GitHub Actions, GitLab CI) убедитесь, что Docker доступен. Пример шага для GitHub Actions:
jobs:
test:
runs-on: ubuntu-latest
services:
docker:
image: docker:24-cli
options: --privileged
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Restore and Test
run: |
dotnet restore
dotnet test --logger "trx;LogFileName=test_results.trx"
Отладка контейнеров:
- Добавьте
WithLoggerв Testcontainers для вывода логов контейнера в консоль тестов. - Используйте
WithExposedPortsи подключитесь к контейнеру через клиент (например, DBeaver) для ручной проверки данных. - Для сложных сценариев применяйте
WithWaitStrategy, чтобы дождаться готовности