Интеграционное тестирование C# с Docker: продвинутый уровень

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

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

Начать курс

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

Блоги

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