Testcontainers .NET для C# (.NET): полное руководство с примерами

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

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

Начать курс

Testcontainers .NET для C# (.NET): полное руководство с примерами

В современной разработке программного обеспечения интеграционное тестирование является критически важным этапом, позволяющим проверить взаимодействие вашего приложения с внешними зависимостями: базами данных, брокерами сообщений, кэшами и другими сервисами. Традиционно для этого разработчики использовали либо заглушки (mocks/stubs), что не даёт полной уверенности в корректности работы, либо поднимали тестовые инстансы сервисов вручную, что сложно, медленно и невоспроизводимо. Библиотека Testcontainers .NET решает эту проблему кардинальным образом: она позволяет программно управлять Docker-контейнерами прямо из ваших тестов, предоставляя лёгкие, одноразовые и изолированные окружения для каждого тестового сценария.

Основная идея Testcontainers заключается в том, чтобы сделать интеграционные тесты такими же простыми и быстрыми, как и модульные. Вы просто пишете тест, в котором объявляете, что вам нужен, например, PostgreSQL или Redis. Библиотека автоматически скачивает соответствующий Docker-образ (если его ещё нет), запускает контейнер, дожидается его готовности и предоставляет вам строку подключения. После завершения теста контейнер автоматически останавливается и удаляется. Это обеспечивает чистоту тестового окружения, отсутствие "мусора" и полную воспроизводимость на любой машине разработчика или CI-сервере.

Testcontainers .NET — это официальный порт популярной Java-библиотеки Testcontainers. Он полностью поддерживает экосистему .NET, включая xUnit, NUnit и MSTest. Библиотека предоставляет готовые модули для большинства популярных сервисов (базы данных, NoSQL, очереди, веб-серверы) и позволяет легко создавать кастомные контейнеры для любых Docker-образов. Использование Testcontainers .NET — это современный стандарт для написания надёжных и качественных интеграционных тестов в .NET-сообществе.

Установка

Для установки библиотеки Testcontainers .NET используется менеджер пакетов NuGet. Выполните следующую команду в терминале в корне вашего проекта или через консоль диспетчера пакетов Visual Studio:

dotnet add package Testcontainers

Эта команда установит основной пакет Testcontainers, который включает в себя базовые классы и поддержку кастомных контейнеров. Для работы с конкретными сервисами (например, PostgreSQL, MSSQL, Redis) вам могут понадобиться дополнительные пакеты. Они устанавливаются аналогично:

dotnet add package Testcontainers.PostgreSql
dotnet add package Testcontainers.Redis
dotnet add package Testcontainers.MsSql
dotnet add package Testcontainers.MongoDb
dotnet add package Testcontainers.RabbitMq

После установки пакетов убедитесь, что на вашей машине запущен Docker Daemon (Docker Desktop для Windows/Mac или Docker Engine для Linux). Testcontainers общается с Docker через его API.

Быстрый старт: минимальный рабочий пример

Давайте напишем простейший тест с использованием xUnit, который запускает контейнер PostgreSQL, создаёт таблицу, вставляет данные и проверяет их чтение. Этот пример демонстрирует базовый принцип работы библиотеки.

using Testcontainers.PostgreSql;
using Xunit;
using Npgsql;

namespace MyIntegrationTests;

public class PostgresQuickStartTest : IAsyncLifetime
{
    private PostgreSqlContainer _postgresContainer;

    // IAsyncLifetime позволяет управлять жизненным циклом контейнера
    public async Task InitializeAsync()
    {
        // 1. Определяем контейнер с настройками по умолчанию
        _postgresContainer = new PostgreSqlBuilder()
            .WithImage("postgres:15-alpine") // Указываем образ
            .WithDatabase("testdb")          // Имя БД
            .WithUsername("testuser")        // Пользователь
            .WithPassword("testpassword")    // Пароль
            .Build();                        // Строим конфигурацию

        // 2. Запускаем контейнер. Метод асинхронный.
        await _postgresContainer.StartAsync();
    }

    [Fact]
    public async Task InsertAndReadData_ShouldSucceed()
    {
        // 3. Получаем строку подключения к контейнеру
        var connectionString = _postgresContainer.GetConnectionString();

        // 4. Используем стандартный ADO.NET для работы с БД
        await using var connection = new NpgsqlConnection(connectionString);
        await connection.OpenAsync();

        // Создаем таблицу
        var createTableCmd = new NpgsqlCommand(
            "CREATE TABLE IF NOT EXISTS users (id SERIAL PRIMARY KEY, name VARCHAR(100))",
            connection);
        await createTableCmd.ExecuteNonQueryAsync();

        // Вставляем данные
        var insertCmd = new NpgsqlCommand(
            "INSERT INTO users (name) VALUES ('Alice')",
            connection);
        await insertCmd.ExecuteNonQueryAsync();

        // Читаем данные
        var selectCmd = new NpgsqlCommand(
            "SELECT name FROM users WHERE id = 1",
            connection);
        var name = (string)await selectCmd.ExecuteScalarAsync();

        // 5. Проверяем результат
        Assert.Equal("Alice", name);
    }

    public async Task DisposeAsync()
    {
        // 6. Останавливаем и удаляем контейнер
        await _postgresContainer.DisposeAsync();
    }
}

Этот минимальный пример показывает ключевые шаги: создание билдера, настройка образа, запуск контейнера, получение строки подключения и автоматическая очистка. Теперь вы можете писать тесты, которые взаимодействуют с реальным PostgreSQL, не устанавливая его локально.

Основные методы и классы

Рассмотрим 8 ключевых классов и методов библиотеки Testcontainers .NET, которые покрывают 90% всех сценариев использования.

1. ContainerBuilder<TBuilderEntity, TContainerEntity> (базовый билдер)

Сигнатура: public abstract class ContainerBuilder<TBuilderEntity, TContainerEntity> where TContainerEntity : DockerContainer

Что делает: Это фундаментальный класс для создания любого Docker-контейнера. Он предоставляет цепочку методов (Fluent API) для настройки образа, портов, переменных окружения, монтирования томов и т.д. Конкретные билдеры (например, PostgreSqlBuilder) наследуются от него.

Пример:

var container = new ContainerBuilder<ContainerBuilder, DockerContainer>()
    .WithImage("nginx:alpine")
    .WithPortBinding(8080, 80) // Проброс порта хоста 8080 на порт контейнера 80
    .WithEnvironment("MY_VAR", "my_value")
    .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(80)) // Ждем, пока порт не станет доступен
    .Build();

2. DockerContainer.StartAsync()

Сигнатура: public Task StartAsync(CancellationToken ct = default)

Что делает: Асинхронно запускает Docker-контейнер. Метод дожидается, пока контейнер полностью запустится и пройдёт все указанные стратегии ожидания (wait strategies). Это блокирующий (асинхронно) вызов, который гарантирует, что сервис готов к работе.

Пример:

await container.StartAsync(); // Контейнер запущен и готов

3. DockerContainer.StopAsync()

Сигнатура: public Task StopAsync(CancellationToken ct = default)

Что делает: Асинхронно останавливает работающий контейнер. В отличие от DisposeAsync, этот метод не удаляет контейнер. Это может быть полезно для тестирования сценариев потери соединения или перезапуска сервиса.

Пример:

await container.StopAsync(); // Контейнер остановлен, но не удален
// ... проверить поведение приложения при недоступности БД ...
await container.StartAsync(); // Запускаем снова

4. DockerContainer.GetConnectionString() (для специализированных контейнеров)

Сигнатура: public string GetConnectionString() (определен в конкретных билдерах, например, PostgreSqlContainer, MsSqlContainer)

Что делает: Возвращает правильно сформированную строку подключения к сервису внутри контейнера. Это избавляет от ручного составления строки на основе имени хоста, порта, имени пользователя и пароля. Доступен только для "известных" типов контейнеров.

Пример:

var connStr = postgresContainer.GetConnectionString();
// connStr = "Host=localhost;Port=5432;Database=testdb;Username=testuser;Password=testpassword"

5. ContainerBuilder.WithWaitStrategy()

Сигнатура: public TBuilderEntity WithWaitStrategy(IWaitStrategy waitStrategy)

Что делает: Устанавливает стратегию ожидания готовности контейнера. По умолчанию Testcontainers ждет, пока контейнер не перейдет в статус "running". С помощью этого метода можно задать более сложные проверки: ожидание лога, ожидание открытия порта, HTTP-запрос и т.д.

Пример:

var container = new ContainerBuilder<...>()
    .WithWaitStrategy(Wait.ForUnixContainer()
        .UntilMessageIsLogged("database system is ready to accept connections")) // Ждем появления строки в логе
    .Build();

6. ContainerBuilder.WithPortBinding()

Сигнатура: public TBuilderEntity WithPortBinding(int hostPort, int containerPort)

Что делает: Позволяет пробросить порт из контейнера на хост-машину. Если не указать hostPort (использовать перегрузку WithPortBinding(int containerPort)), то порт будет назначен динамически (случайный свободный порт на хосте). Это стандартный способ сделать сервис доступным из тестового кода.

Пример:

// Проброс с фиксированным портом (может вызвать конфликт, если порт занят)
builder.WithPortBinding(5432, 5432);

// Проброс со случайным портом (рекомендуется для параллельных тестов)
builder.WithPortBinding(5432); // Порт на хосте будет случайным

7. ContainerBuilder.WithVolumeMount()

Сигнатура: public TBuilderEntity WithVolumeMount(string source, string destination, AccessMode accessMode = AccessMode.ReadWrite)

Что делает: Монтирует Docker-том или директорию с хост-машины в контейнер. Это может быть полезно для передачи конфигурационных файлов, скриптов инициализации БД или сохранения данных между запусками контейнера.

Пример:

// Монтируем локальную папку со скриптами SQL в контейнер
builder.WithVolumeMount(
    Path.GetFullPath("./init-scripts"), // Абсолютный путь на хосте
    "/docker-entrypoint-initdb.d/");    // Путь внутри контейнера

8. ContainerBuilder.WithImage()

Сигнатура: public TBuilderEntity WithImage(string image)

Что делает: Задает Docker-образ, который будет использован для создания контейнера. Можно указать тег (например, postgres:15-alpine, redis:7). Если образ не найден локально, Testcontainers автоматически скачает его из реестра (Docker Hub по умолчанию).

Пример:

builder.WithImage("mcr.microsoft.com/mssql/server:2022-latest"); // Образ из Microsoft Container Registry

Реальный практический пример: тестирование ASP.NET Core Web API с PostgreSQL

Рассмотрим полноценный сценарий: у нас есть ASP.NET Core Web API, который использует Entity Framework Core для работы с PostgreSQL. Мы хотим написать интеграционный тест для контроллера, который создает и возвращает пользователей. В этом примере мы используем xUnit, Testcontainers и WebApplicationFactory для поднятия всего приложения в памяти.

using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Testcontainers.PostgreSql;
using Xunit;

namespace MyIntegrationTests;

// Пример модели
public class User
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public string Email { get; set; } = string.Empty;
}

// Пример DbContext
public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
    public DbSet<User> Users => Set<User>();
}

// Пример контроллера
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
    private readonly AppDbContext _context;
    public UsersController(AppDbContext context) => _context = context;

    [HttpPost]
    public async Task<ActionResult<User>> CreateUser(User user)
    {
        _context.Users.Add(user);
        await _context.SaveChangesAsync();
        return CreatedAtAction(nameof(GetUser), new { id = user.Id }, user);
    }

    [HttpGet("{id}")]
    public async Task<ActionResult<User>> GetUser(int id)
    {
        var user = await _context.Users.FindAsync(id);
        if (user == null) return NotFound();
        return user;
    }
}

// Интеграционный тест
public class UsersControllerIntegrationTest : IClassFixture<WebApplicationFactory<Program>>, IAsyncLifetime
{
    private readonly WebApplicationFactory<Program> _factory;
    private PostgreSqlContainer _postgresContainer;

    public UsersControllerIntegrationTest(WebApplicationFactory<Program> factory)
    {
        _factory = factory;
    }

    public async Task InitializeAsync()
    {
        // 1. Запускаем PostgreSQL в контейнере
        _postgresContainer = new PostgreSqlBuilder()
            .WithImage                    

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