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

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

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

Начать курс

Интеграционное тестирование 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, где окружение должно быть чистым.

Интеграция с CI/CD

Блоги

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