Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → Testcontainers: интеграционные тесты с настоящими базами в Docker

Testcontainers: интеграционные тесты против настоящих PostgreSQL, Redis и Kafka

Как тестировать интеграции честно: Testcontainers поднимает одноразовые контейнеры БД и брокеров прямо из тестов, устраняя расхождение dev/prod и ручную подготовку окружения.

Прямой ответ

Testcontainers решает главную проблему интеграционных тестов: тестам нужна настоящая база/брокер, а не эмуляция, и окружение должно создаваться и уничтожаться автоматически. Библиотека (Java, Python, Node, .NET, Go) из кода теста поднимает одноразовый Docker-контейнер нужной версии, отдаёт приложению строку подключения, а после прогона контейнер уничтожается. Результат: тесты одинаковы на ноутбуке, у коллеги и в CI; версия БД в тесте совпадает с продом одной строкой; «работало на моей машине» исчезает как класс. Требования: Docker на машине прогона и 1–2 ГБ свободного RAM на контейнеры.

Пример: Python + pytest + PostgreSQL

import pytest
from testcontainers.postgres import PostgresContainer

@pytest.fixture(scope="session")
def pg():
    with PostgresContainer("postgres:16-alpine") as p:
        yield p.get_connection_url()

def test_migrations_and_insert(pg):
    # применяем миграции к pg, выполняем запросы — всё как в проде
    ...

Что происходит под капотом: контейнер стартует, библиотека ждёт readiness (health-проверка), тест получает динамический порт на localhost — параллельные прогоны не конфликтуют; по выходе контейнер удаляется вместе с данными. Java-вариант (@Container + JUnit) и Node (@testcontainers/postgresql) устроены идентично — концепции переносятся между языками.

Когда testcontainers, а когда моки

Граница ответственности простая и важная: моки (msw/WireMock из статьи про тестирование API) проверяют вашу логику против ожидаемого контракта — быстро, без инфраструктуры. Testcontainers проверяют саму интеграцию: SQL-диалект и миграции, поведение транзакций и блокировок, сериализацию в Redis, доставку в брокер. Оба слоя нужны: юнит-тесты с моками гоняются на каждый коммит за секунды; интеграционные с контейнерами — на PR/merge, несколько минут. Замена одного слоя другим — частая ошибка: моки не ловят «запрос невалиден для PostgreSQL 16», а контейнеры на каждый чих замедляют цикл до невыносимости.

Паттерны, которые экономят время прогона

  1. Session-scope контейнеры: БД поднимается один раз на прогон, между тестами — очистка данных (truncate/транзакционный откат), а не новый контейнер на тест. Подъём PostgreSQL — секунды, но на сотне тестов разница ощутима.
  2. Переиспользование (reuse): флаг withReuse(true)/TC_REUSE оставляет контейнер живым между локальными прогонами — итерация ускоряется кратно; в CI reuse выключают (чистота важнее скорости).
  3. Миграции в фикстуре: применяйте Alembic/Flyway к свежему контейнеру — миграции тестируются автоматически на каждом прогоне, ловя расхождение схемы и кода.
  4. Параллельность: динамические порты позволяют гнать наборы параллельно — но считайте RAM: четыре PostgreSQL-контейнера — это ~1,5–2 ГБ.

Ресурсы и CI

Локально: Docker + 2–4 ГБ на контейнеры — обычный бюджет из статьи про Compose-стек; на 8 ГБ ноутбуке держите последовательный прогон. В CI варианты: hosted-раннеры GitHub Actions имеют Docker из коробки; self-hosted — по настройке из статьи про свой раннер. Оптимизация CI-времени: параллельные джобы с независимыми контейнерами и кэш образов (pull по тегу — секунды, если слой уже на раннере).

Честное ограничение: testcontainers требует Docker — на корпоративных машинах с политиками без Docker Desktop используйте альтернативные движки (Podman с docker-socket-эмуляцией, colima на macOS); на Windows без WSL2 — только Docker Desktop, нативных контейнеров нет.

Соседство с локальной dev-базой

Важно не путать роли: контейнеры testcontainers — для тестов, постоянно живущий PostgreSQL из статьи про локальную базу — для ручной разработки и отладки. Строка подключения приложения выбирается конфигом: DATABASE_URL на dev-базу при ручном прогоне, на testcontainers-инстанс — в тестах. Смешивание (тесты против dev-базы) — источник «зелёных локально, красных в CI» прогонов: dev-база накапливает состояние, которого нет в CI.

FAQ

Насколько testcontainers замедляет тесты? Первый старт контейнера — 2–10 с на образ; с reuse и session-scope стоимость размывается. Интеграционный набор из 50 тестов против одного PostgreSQL обычно укладывается в минуту.

Можно ли без Docker, через podman? Да, при включённой docker-совместимости (podman socket); официальный статус поддержки сверяйте с документацией вашей языковой библиотеки — зрелость варьируется.

Что делать с тестами, которым нужен внешний платный API? Не поднимать его в контейнере, а мокать на границе (WireMock-контейнер, кстати, тоже поднимается через testcontainers — мок с настраиваемыми сценариями как инфраструктура теста).

Testcontainers или docker compose up перед тестами вручную? Compose-стек «вручную» — это расхождение версий, забытые cleanup и конфликтующие порты у команды. Testcontainers кодирует окружение в самих тестах — это и есть инфраструктура-как-код в масштабе тестового контура.

Готовый контур: как это выглядит в проекте

Минимальная, но полноценная схема на Python-проекте: фикстура session-scope поднимает PostgreSQL 16, функция-фикстура на каждый тест открывает транзакцию и откатывает её в конце — данные не накапливаются, контейнер один:

@pytest.fixture
def db_session(pg):
    with engine(pg).connect() as conn:
        trans = conn.begin()
        yield Session(bind=conn)
        trans.rollback()

Слои тестов при такой организации выстраиваются сами: чистые unit-тесты без фикстур — миллисекунды, гоняются на каждый save; тесты с db_session — быстрый интеграционный слой; тесты с отдельными контейнерами (Kafka, внешний API через WireMock) — тяжёлый слой на merge. Маркировка pytest (@pytest.mark.integration) позволяет в CI гонять слои отдельными джобами параллельно — суммарное время контура падает кратно против одного последовательного прогона.

Показатель здоровья контура: локальный прогон интеграционного слоя занимает минуты, а не «пойду пообедаю». Если дольше — ищите лишние контейнеры на тест и отсутствующий reuse; почти всегда дело в них.

Что делать, если в CI нет Docker-in-Docker? Варианты по порядку предпочтения: раннер с доступом к сокету хостового Docker (host-mount), rootless-режимы, либо перенос интеграционного слоя на отдельную джобу с нужным окружением. Сервисы compose из workflow GitHub Actions тоже работают — но тогда окружение кодируется в двух местах, и расхождения неизбежны.

Как отлаживать упавший интеграционный тест? С флагом reuse контейнер остаётся живым после падения — подключайтесь и смотрите состояние (psql, redis-cli внутрь контейнера). Без reuse помогайте себе логами: библиотеки умеют печатать stdout контейнера при ошибке; включите эту опцию в отладочном режиме.

Можно ли использовать testcontainers для локальной разработки, а не только тестов? Технически да, но это подмена ролей: контейнеры библиотеки одноразовые и управляются кодом. Для постоянной локальной среды правильнее обычный compose-стек — он переживает перезапуски и настраивается под человека, а не под прогон.

Источники: testcontainers.com, документация Java/Python-библиотек (ссылки в шапке).