Перейти к содержимому

Написание интеграционных тестов

dwe test запускает deploy-пайплайн вашего проекта — и любые проверки или команды, которые вы добавите — внутри свежей одноразовой копии проекта. Ничто из происходящего здесь не затрагивает окружение, в котором вы работаете. Это руководство по процессу авторства; пофайловая схема живёт в ../reference/config/tests.md.

dwe test выдаёт каждой изолированной копии свои собственные host-порты, поэтому сценарий работает параллельно с вашим рабочим окружением — и любым другим проектом, занявшим эти порты. Каждый host-порт, объявленный вашими включёнными сервисами под services.<name>.ports, автоматически переназначается на свежевыделенный свободный порт в копии — вы ничего не проводите через vars и не пишете конфигурацию портов в сценарии. И preflight-проверка ports_free, и — для проектов, которые берут свои compose-биндинги из services.<name>.ports (напрямую или через запись exports.env from: services.<name>.ports.<x>) — фактический бинд контейнера читают порт из того же места, поэтому переназначение двигает их вместе.

Шаг сценария, которому нужен переназначенный порт, ссылается на него обычным образом: ${services.<name>.ports.<x>}.

Единственный случай, который это не покрывает, — host-порт, захардкоженный прямо в сыром compose-файле (8080:8080), который ваша dwe-конфигурация сервиса вообще не моделирует — он обходит и переназначение, и preflight ports_free. Либо объявите его под services.<name>.ports, чтобы dwe test мог его увидеть и переназначить, либо проведите подстановку в compose через var и задайте этот var на сценарий через env.vars: { …: auto } (раннер выделит свободный порт и запишет его в vars: копии; шаг тогда читает ${vars.<path>}).

Создайте workspace/tests/smoke.yml:

description: "Clean deploy comes up healthy"
steps:
- name: "app answers"
type: builtin
cmd: http_check
with:
url: "http://localhost:${services.app.ports.http}/health"
status: 200

Запустите:

Окно терминала
dwe test run smoke

Это копирует ваш проект в изолированное дерево, генерирует свежий local.yml со свежевыделенными свободными host-портами, выполняет dwe validate, затем реальный dwe deploy run внутри копии, проверяет endpoint и полностью сносит всё после себя. Сценарий вообще без steps: уже полезен как тест — “деплой с этими параметрами проходит успешно”.

env: описывает, чем окружение этого сценария отличается от значений по умолчанию — переключатели сервисов и переопределения vars:

description: "Deploy with redis disabled — cache falls back to in-memory"
env:
services:
disable: [redis]
steps:
- name: "app still answers without redis"
type: builtin
cmd: http_check
with:
url: "http://localhost:${services.app.ports.http}/health"
status: 200

Каждый файл сценария — это одно изолированное окружение — пишите по одному на каждый значимый вариант (выключенный сервис, переключённый feature-флаг var, другая раскладка портов), а не пытайтесь параметризовать единственный файл.

Шаги используют ровно ту же схему, что и workspace/deploy.yml: type: shell, type: dwe, type: command, type: builtin, с условиями when:. Любой билтин-предикат — file_exists, tcp_reachable, containers_running, env_keys_present, http_check — работает напрямую как тело шага и ведёт себя как проверка pass/fail, а не только внутри check::

steps:
- name: "containers are up"
type: builtin
cmd: containers_running
with: { services: [app, db] }
- name: "app answers"
type: builtin
cmd: http_check
with: { url: "http://localhost:${services.app.ports.http}/health", status: 200, contains: "ok" }

${...} в with:/cmd: шага резолвится по конфигу копии до выполнения шага, а пути вида file_exists резолвятся относительно корня копии — проверки всегда смотрят на одноразовое окружение, а не на ваше рабочее дерево.

Тестирование проектной команды через type: command

Заголовок раздела «Тестирование проектной команды через type: command»

Второй сценарий использования из спецификации — “создать дамп БД, проверить, что файл появился” — это просто шаг type: command, вызывающий обычную пользовательскую команду, за которым следует проверка:

steps:
- name: "create dump"
type: command
cmd: db:dump
- name: "dump file exists"
type: builtin
cmd: file_exists
with: { path: "dumps/db-latest.sql.gz" }

Шаги type: command могут вызывать private-команды, так что вы можете держать тест-специфичные команды (например, команду, засеивающую фикстуры, или дампящую в файл с фиксированным именем вместо снабжённого timestamp’ом) вне повседневного листинга dwe commands:

workspace/commands/testing.yml
group: testing
commands:
- id: db:dump
private: true
type: shell
cmd: "docker compose exec -T db pg_dump -U app app > dumps/db-latest.sql.gz"

(hide-команды пайплайны пропускают полностью, поэтому здесь они не работают — используйте private для тест-специфичных команд, которые всё же должны быть запускаемы из сценария.)

Когда сценарий проваливается (сбой деплоя, сбой шага или таймаут), runner пытается собрать отчёт об ошибке в .dwe/tests/reports/<scenario>/ — автоматически, до выполнения teardown, так что он переживает снос окружения. Сбор best-effort: если директорию отчёта создать не удалось, путь остаётся пустым, а отдельные артефакты могут оказаться частичными или пустыми при сбое захвата — но запуск в любом случае сообщает о провале.

Окно терминала
dwe test run smoke # проваливается
ls .dwe/tests/reports/smoke/
# pipeline.log — лог пайплайна деплоя/шагов сценария
# compose-ps.txt — docker compose ps --all внутри копии
# container-logs.txt — объединённые логи контейнеров (последние 200 строк каждого)

Это тот самый отчёт, который стоит приложить к упавшему CI-запуску или прочитать локально, вообще не трогая Docker. Он перезаписывается на каждом непройденном запуске, поэтому всегда отражает последний сбой.

Когда отчёта недостаточно и нужно осмотреть само живое окружение, перезапустите с --keep:

Окно терминала
dwe test run --keep smoke

Teardown пропускается (и отчёт об ошибке не собирается, поскольку само окружение сохраняется); dwe test run печатает имя compose-проекта и путь копии, чтобы вы могли зайти внутрь (cd), осмотреть контейнеры через docker compose -p <project> ps или открыть шелл внутри сервиса. Манифест тоже остаётся на диске, поэтому повторный dwe test run smoke откажется стартовать, пока вы не уберёте всё вручную.

Уберите сохранённый запуск — или что угодно, оставшееся после упавшего запуска — командой dwe test clean:

Окно терминала
dwe test clean --dry-run # посмотреть, что было бы удалено, ничего не трогая
dwe test clean smoke # снести сохранённое/осиротевшее окружение именно этого сценария
dwe test clean # снести каждое манифестированное окружение

clean манифест-управляемый и никогда не угадывает имя compose-проекта: он сносит только те окружения, для которых есть манифест, пропускает любой сценарий, чей flock в данный момент удерживается живым запуском, и отдельно (report-only) перечисляет Docker compose-проекты, похожие на вывод dwe test, но без манифеста — их он лишь помечает для ручного удаления, не снося автоматически.

Ограничение медленного или зависшего шага

Заголовок раздела «Ограничение медленного или зависшего шага»

Любой шаг может иметь собственный timeout: — то же опциональное поле движка, что и у шагов deploy.yml. Полезно, когда шаг сценария — это проверка tcp_reachable/http_check (или команда shell), которая должна быстро провалиться, а не выполняться до истечения общего timeout: сценария:

steps:
- name: "app answers quickly"
type: builtin
cmd: http_check
timeout: 5s
with:
url: "http://localhost:${services.app.ports.http}/health"
status: 200

Отсутствие поля или timeout: 0 оставляет шаг неограниченным. Таймаут ограничивает только тело шага, которое учитывает отмену контекста Go — шаги-подпроцессы (type: shell/type: dwe) и учитывающие ctx билтины покрыты; шаг, заблокированный на интерактивном вводе, не прерывается принудительно (здесь это неактуально, поскольку запуски сценариев всегда неинтерактивны). Полный контракт см. в Полях шага.

Отлов ошибок до того, как что-либо запущено

Заголовок раздела «Отлов ошибок до того, как что-либо запущено»

dwe validate tests статически проверяет каждый файл workspace/tests/*.yml — имя сценария, парсинг timeout:, ссылки env.services, схему шага (включая параметры with: билтина и условия when:) и ссылки type: command — не трогая Docker и не поднимая копию. Это достаточно дёшево, чтобы запускать на каждом push в CI, перед куда более медленным dwe test run:

Окно терминала
dwe validate tests

Он также выводит риски изоляции compose как предупреждения, так что вы можете исправить их до того, как на них заблокируется dwe test run.

dwe test run сканирует compose-файлы копии на конструкции, которые обходят разграничение compose по имени проекта — container_name: и буквальные (нешаблонизированные) host-порты блокирующие; volume’ы и сети с external:/явным name: — только предупреждения. Блокирующая находка проваливает сценарий ещё до начала деплоя (teardown всё равно выполняется), с сообщением, называющим проблемную конструкцию:

blocking compose isolation hazard(s), refusing to run: service db sets container_name: myapp-db — bypasses compose project-name scoping and collides with any other project/run using the same fixed name — pass --skip-isolation-check to downgrade to a warning

Исправляйте у источника, когда это возможно:

  • Буквальный host-порт (8080:8080 в сыром compose-файле) — перенесите его в services.<name>.ports, чтобы dwe test мог увидеть его и переназначить автоматически (см. Порты изолируются автоматически выше), либо проведите подстановку в compose через var, заданный через env.vars: { …: auto }.
  • container_name: — уберите его; compose уже детерминированно именует контейнеры по имени проекта + сервиса, и именно фиксированный container_name: вызывает столкновение.

Когда находка ложноположительна, или исправить её прямо сейчас непрактично, понизьте каждую находку до предупреждения и продолжайте:

Окно терминала
dwe test run --skip-isolation-check smoke

См. Сканер изоляции compose для полного списка отмечаемых конструкций и градации fail/warn.

Окно терминала
dwe test run # каждый сценарий под workspace/tests/*.yml, отсортированные
dwe test run smoke db-dump # только эти два, по имени
dwe test list # имена сценариев + описания
dwe test run --timeout 5m # переопределить собственный timeout: каждого сценария
dwe test run --skip-isolation-check smoke # понизить блокирующие находки изоляции до предупреждений
dwe test run --parallel 3 # запускать до 3 сценариев одновременно

Коды выхода делают это CI-дружелюбным из коробки: 0 — всё прошло, 1 — хотя бы один сценарий провалился, 2 — сценарий не удалось даже подготовить (плохое имя, захваченный lock, ошибка файла сценария). --output json даёт машиночитаемый отчёт о том же результате.

Каждый сценарий уже выполняется полностью изолированно — своя копия, свой compose-проект, свои автоматически перемапленные host-порты — так что ничто не мешает запустить несколько одновременно. --parallel N запускает до N сценариев параллельно:

Окно терминала
dwe test run --parallel 3 # до 3 одновременно, по всему набору
dwe test run --parallel 2 smoke redis-off # эти два, бок о бок

Эффективный параллелизм — min(N, число сценариев), поэтому --parallel 8 на трёх сценариях запускает три воркера. При более чем одном воркере потоковый вывод каждого сценария заменяется компактным живым видом — одна строка на сценарий со спиннером, грубой фазой и временем, финализирующаяся в ✓ <name> passed либо ✗ <name> failed — step "…":

✓ [12s] smoke passed
⠹ [4s] redis-off deploying…
⠹ [6s] cache-on running steps…
running 2/3 scenarios…

Полный лог деплоя/пайплайна каждого сценария по-прежнему идёт в собственную копию (.dwe/tests/runs/<scenario>/.dwe/logs/test.log), а отчёт непройденного сценария собирается ровно как при последовательном запуске. Запуски через pipe/CI (без TTY) печатают плоские строки scenario <name>: started / scenario <name>: passed вместо живого блока; --output json не зависит от --parallel.

--parallel 1 (по умолчанию) сохраняет сегодняшнее поведение байт-в-байт: полный потоковый вывод пайплайна, по одному сценарию за раз.

Следите за общими кэшами. Сценарии, которые все долбят один и тот же shared: true кэш пакетов (composer, npm), могут конкурировать при совместном запуске — установки на холодный кэш сериализуются на собственном lock-файле пакетного менеджера. Параллельте сценарии с тёплым или непересекающимся кэшем свободно; тяжёлые установки на холодный кэш держите последовательными и подбирайте N под то, что ваш Docker-демон комфортно тянет одновременно.

  • ../reference/config/tests.md — полная схема сценария, модель изоляции, структура .dwe/tests/, порядок teardown, dwe validate tests, сканер изоляции compose, коды выхода, документированные ограничения.
  • ../reference/config/deploy/index.md — полная таблица полей шага, включая общее поле timeout:.
  • ../reference/config/deploy/builtins.md — каждый билтин, доступный steps:, включая http_check и семантику предиката-как-проверки.
  • author-project-commands.md — авторство шагов type: command, которые может вызывать сценарий, включая private-команды.
  • preflight-checks.md — проверка ports_free, которая обеспечивает предпосылку проведённых через vars портов.