Интеграционное тестирование C# с Docker: как избавиться от проблем с окружением
Каждый разработчик C# сталкивался с ситуацией: тесты проходят на локальной машине, но падают на сервере CI. Причина чаще всего — различия в окружении: версия базы данных, настройки Redis, отсутствие тестовых сервисов. Интеграционное тестирование C# с Docker решает эту проблему раз и навсегда. В этой статье мы разберем, как поднять изолированное тестовое окружение с помощью контейнеров, написать настоящие интеграционные тесты и интегрировать их в CI/CD пайплайн.
Почему Docker — идеальный инструмент для интеграционных тестов C#
Интеграционное тестирование проверяет взаимодействие между компонентами системы: приложение + база данных, API + кэш, микросервисы между собой. Без Docker вам пришлось бы:
- Устанавливать PostgreSQL, SQL Server, Redis на каждую машину разработчика
- Настраивать CI-сервер с теми же версиями
- Чистить данные между тестовыми прогонами
Docker решает все эти задачи. Вы описываете инфраструктуру в коде (Dockerfile, docker-compose.yml), и она разворачивается одинаково на любой системе. Для C# (.NET) это особенно актуально, так как Microsoft активно поддерживает контейнеризацию: официальные образы .NET SDK, SQL Server в Linux-контейнере, интеграция с Azure DevOps.
Настройка проекта: Testcontainers + xUnit
Для интеграционного тестирования C# с Docker мы будем использовать библиотеку Testcontainers (доступна на NuGet). Она автоматически управляет жизненным циклом контейнеров: запускает перед тестами, останавливает после завершения.
Шаг 1. Установка пакетов
Создайте новый проект для тестов или откройте существующий. Добавьте NuGet-пакеты:
dotnet add package Testcontainers
dotnet add package Testcontainers.PostgreSql
dotnet add package xunit
dotnet add package FluentAssertions
Шаг 2. Базовый класс для тестов с контейнерами
Создадим абстрактный класс, который будет поднимать PostgreSQL перед запуском тестов:
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:15-alpine")
.WithDatabase("testdb")
.WithUsername("test")
.WithPassword("test123")
.Build();
}
public async Task InitializeAsync()
{
await _postgresContainer.StartAsync();
}
public async Task DisposeAsync()
{
await _postgresContainer.StopAsync();
}
}
Класс реализует IAsyncLifetime из xUnit — контейнер запустится до первого теста и остановится после последнего. Метод GetConnectionString() возвращает динамический порт, который Docker назначил контейнеру.
Шаг 3. Пишем интеграционный тест
Теперь напишем тест, который проверяет работу репозитория с реальной базой данных:
public class UserRepositoryTests : IntegrationTestBase
{
[Fact]
public async Task CreateUser_Should_SaveToDatabase()
{
// Arrange
await using var connection = new NpgsqlConnection(ConnectionString);
await connection.OpenAsync();
var repository = new UserRepository(connection);
var user = new User { Name = "Иван", Email = "ivan@example.com" };
// Act
var userId = await repository.CreateAsync(user);
// Assert
userId.Should().BeGreaterThan(0);
var savedUser = await repository.GetByIdAsync(userId);
savedUser.Should().NotBeNull();
savedUser.Name.Should().Be("Иван");
}
}
Обратите внимание: мы не мокаем базу данных. Тест реально вставляет запись в PostgreSQL, запущенный в Docker-контейнере. Это и есть суть интеграционного тестирования C# с Docker — проверка реального взаимодействия компонентов.
Продвинутые сценарии: несколько контейнеров и миграции
В реальных проектах редко используется только одна база данных. Часто нужны Redis для кэша, RabbitMQ для очередей, а также миграции схемы (Entity Framework, FluentMigrator). Testcontainers поддерживает все популярные сервисы.
Пример: приложение + PostgreSQL + Redis
public class MultiContainerFixture : IAsyncLifetime
{
private readonly PostgreSqlContainer _db;
private readonly RedisContainer _redis;
public string DbConnectionString => _db.GetConnectionString();
public string RedisConnectionString => _redis.GetConnectionString();
public MultiContainerFixture()
{
_db = new PostgreSqlBuilder()
.WithImage("postgres:15-alpine")
.Build();
_redis = new RedisBuilder()
.WithImage("redis:7-alpine")
.Build();
}
public async Task InitializeAsync()
{
await Task.WhenAll(_db.StartAsync(), _redis.StartAsync());
}
public async Task DisposeAsync()
{
await Task.WhenAll(_db.StopAsync(), _redis.StopAsync());
}
}
Используйте Task.WhenAll для параллельного запуска контейнеров — это ускоряет выполнение тестов.
Автоматическое выполнение миграций
Перед каждым тестом нужно привести базу к известному состоянию. Используйте DbUp или EF Core:
public class DatabaseFixture : IntegrationTestBase
{
protected async Task ApplyMigrations()
{
await using var connection = new NpgsqlConnection(ConnectionString);
await connection.OpenAsync();
var migrator = new DbUpMigrator(connection);
await migrator.MigrateUpAsync();
}
}
public class ProductRepositoryTests : DatabaseFixture
{
[Fact]
public async Task GetProducts_Should_ReturnData()
{
await ApplyMigrations();
// Тест с чистой схемой базы данных
// ...
}
}
Интеграция в CI/CD: GitHub Actions + Docker
Интеграционное тестирование C# с Docker отлично работает в CI. Главное преимущество — вам не нужно устанавливать Docker внутри контейнера (Docker-in-Docker). В GitHub Actions Docker уже предустановлен на всех раннерах.
Пример workflow для .NET
name: Integration Tests
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0'