Как сделать автотесты, которые можно запускать в Docker в разных средах, включая неконтролируемые
Что проверяют
Умение сделать образ с тестами воспроизводимым и переносимым: конфигурация снаружи, зависимости зафиксированы, результат выезжает наружу, а код возврата честно валит пайплайн. «Неконтролируемая среда» в вопросе означает стенд, где данные меняет кто-то ещё и сервисы поднимаются не мгновенно.
Как отвечать
Образ. Базовый образ фиксируется конкретным тегом, зависимости ставятся из lock-файла, а не строкой pip install pytest requests (иначе завтра приедет другая мажорная версия). Слои раскладываются так, чтобы установка зависимостей кешировалась.
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY tests ./tests
ENTRYPOINT ["pytest", "--junitxml=/app/reports/junit.xml"]ENTRYPOINT вместо CMD позволяет дописывать аргументы при запуске: docker run --rm image -m smoke -k login.
Один образ на все среды. Ни URL, ни логины, ни имя стенда внутрь не зашиваются: всё через переменные окружения и --env-file. Секреты не кладутся ни в слои, ни в git.
services:
tests:
build: .
environment:
BASE_URL: ${BASE_URL:?BASE_URL не задан}
TZ: UTC
shm_size: 2gb
volumes:
- ./reports:/app/reportsКлюч version: в Compose v2 устарел и только даёт предупреждение, его пишут по инерции. Монтировать сами тесты томом стоит только локально: в CI образ и есть артефакт, а том маскирует то, что файл не попал в сборку.
Код возврата. docker compose up возвращает ноль, даже если тесты упали, поэтому пайплайн зеленеет на красных тестах. Правильно: docker compose run --rm tests или docker compose up --abort-on-container-exit --exit-code-from tests.
Устойчивость в чужой среде:
- зависимости поднимаются через
healthcheckиdepends_on: condition: service_healthy, ожидание готовности делается опросом с таймаутом, а неsleep 30; - тесты не предполагают пустую базу: свои данные создаются с уникальным префиксом (uuid, метка прогона) и убираются в фикстурах, проверки пишутся относительно созданного, а не относительно «первой строки в списке»;
- прогон должен переживать параллельный запуск другого прогона по тому же стенду, отсюда запрет на глобальные операции вроде очистки таблицы;
- фиксируются
TZи локаль, иначе тесты на даты ведут себя по-разному на машине разработчика и на раннере; - для UI-тестов берутся официальные образы Playwright или Selenium со шрифтами и браузерами внутри, и обязательно поднимается
shm_size(или--ipc=host): дефолтные 64 МБ/dev/shmроняют Chrome с непонятными ошибками; - на Apple Silicon для образов без arm-варианта нужен явный
--platform=linux/amd64.
Результаты наружу. JUnit XML и Allure-результаты пишутся в примонтированный каталог, иначе после удаления контейнера отчёта не останется. Скриншоты, видео и логи прикладываются к упавшим тестам там же.
Где спотыкаются
Собирают образ, который тянет зависимости без версий и знает адрес одного конкретного стенда, после чего на другой среде он не работает. И запускают контейнер так, что упавшие тесты не роняют пайплайн, потому что код возврата теряется в docker compose up.