Интеграционное тестирование C# с Docker: полное руководство

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Интеграционное тестирование 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'                

Блоги

Рекомендаций по книгам