Интеграционное тестирование C# с Docker: от А до Я
Интеграционное тестирование — критически важный этап разработки любого серьёзного приложения на C#. Оно проверяет, как различные компоненты системы (база данных, внешние API, файловые системы) работают вместе. Однако настройка тестовой среды часто превращается в головную боль: нужно поднимать реальные серверы, чистить данные после тестов и бороться с нестабильностью окружения. Docker решает эти проблемы, предоставляя лёгкие, изолированные и воспроизводимые контейнеры. В этой статье мы разберём, как настроить интеграционное тестирование C# с Docker от нуля до рабочего пайплайна.
Почему Docker идеален для интеграционных тестов на C#?
Традиционный подход — использовать локальные базы данных SQL Server или эмуляторы внешних сервисов. Это приводит к проблемам:
- Загрязнение данных: тесты оставляют мусор в БД, влияя на другие тесты.
- Несовместимость окружений: на машине разработчика одна версия СУБД, на CI-сервере — другая.
- Сложность параллельного запуска: тесты конфликтуют за порты и ресурсы.
Docker решает это просто: каждый тест (или группа тестов) получает свой изолированный контейнер с нужной версией БД, Redis или другого сервиса. Контейнеры лёгкие, запускаются за секунды и гарантированно уничтожаются после завершения тестов. Для .NET-разработчиков это особенно актуально благодаря библиотеке Testcontainers for .NET, которая автоматизирует управление контейнерами прямо из кода тестов.
Настройка проекта: добавляем Docker в тесты C#
Для начала создадим проект с интеграционными тестами. Предположим, у нас есть веб-API на .NET 8, которое работает с PostgreSQL. Нам нужно протестировать эндпоинт создания пользователя.
Шаг 1: Установка NuGet-пакетов
Добавьте в тестовый проект следующие пакеты:
dotnet add package Testcontainers.PostgreSql
dotnet add package Microsoft.AspNetCore.Mvc.Testing
dotnet add package FluentAssertions
Testcontainers.PostgreSql — это обёртка над Docker SDK, которая управляет контейнерами PostgreSQL. Microsoft.AspNetCore.Mvc.Testing позволяет запускать веб-приложение в памяти для тестирования.
Шаг 2: Создание базового класса для тестов
Создадим абстрактный класс, который будет поднимать контейнер с БД перед тестами:
using Testcontainers.PostgreSql;
using Xunit;
public abstract class IntegrationTestBase : IAsyncLifetime
{
private readonly PostgreSqlContainer _postgresContainer;
protected string ConnectionString => _postgresContainer.GetConnectionString();
protected IntegrationTestBase()
{
_postgresContainer = new PostgreSqlBuilder()
.WithImage("postgres:16-alpine")
.WithDatabase("testdb")
.WithUsername("test")
.WithPassword("test123")
.Build();
}
public async Task InitializeAsync()
{
await _postgresContainer.StartAsync();
}
public async Task DisposeAsync()
{
await _postgresContainer.StopAsync();
}
}
Этот класс реализует IAsyncLifetime из xUnit, что гарантирует запуск контейнера до всех тестов и его остановку после. ConnectionString динамически получается из контейнера — порт может быть любым свободным.
Шаг 3: Написание теста с Testcontainers и WebApplicationFactory
Теперь напишем сам тест. Мы будем использовать WebApplicationFactory для создания тестового HTTP-клиента:
using Microsoft.AspNetCore.Mvc.Testing;
using FluentAssertions;
using System.Net.Http.Json;
public class UserApiTests : IntegrationTestBase
{
private readonly WebApplicationFactory<Program> _factory;
public UserApiTests()
{
_factory = new WebApplicationFactory<Program>()
.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
// Заменяем реальную строку подключения на тестовую
var descriptor = services.SingleOrDefault(
d => d.ServiceType == typeof(DbContextOptions<AppDbContext>));
if (descriptor != null)
services.Remove(descriptor);
services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(ConnectionString));
});
});
}
[Fact]
public async Task CreateUser_ShouldReturnCreatedUser()
{
// Arrange
var client = _factory.CreateClient();
var newUser = new { Name = "Test User", Email = "test@example.com" };
// Act
var response = await client.PostAsJsonAsync("/api/users", newUser);
// Assert
response.StatusCode.Should().Be(System.Net.HttpStatusCode.Created);
var createdUser = await response.Content.ReadFromJsonAsync<User>();
createdUser.Should().NotBeNull();
createdUser.Name.Should().Be("Test User");
}
}
Ключевой момент: в WithWebHostBuilder мы подменяем сервис DbContext на тот, что использует строку подключения из контейнера PostgreSQL. Теперь тест работает с изолированной БД, которая автоматически создаётся и удаляется.
Продвинутые сценарии: Docker Compose и несколько контейнеров
Часто одного контейнера недостаточно. Например, приложение может зависеть от PostgreSQL, Redis и стороннего API. В таких случаях удобно использовать Docker Compose внутри теста. Testcontainers поддерживает это через DockerComposeContainer.
Пример: тестирование с Redis и PostgreSQL
using Testcontainers.Compose;
public class MultiServiceTest : IAsyncLifetime
{
private DockerComposeContainer _compose;
public async Task InitializeAsync()
{
_compose = new DockerComposeContainer()
.WithProjectFile("docker-compose.test.yml")
.Build();
await _compose.StartAsync();
}
public async Task DisposeAsync()
{
await _compose.StopAsync();
}
[Fact]
public async Task TestWithRedisAndPostgres()
{
// Получаем адреса сервисов
var postgresUrl = _compose.GetServiceAddress("postgres", 5432);
var redisUrl = _compose.GetServiceAddress("redis", 6379);
// ... используем их в приложении
}
}
Файл docker-compose.test.yml определяет все необходимые сервисы. Testcontainers автоматически поднимает и опускает их для каждого тестового класса. Это идеально для CI/CD, где окружение должно быть чистым.