12k
All articles

Smoke-тесты и почему агенты продолжают их писать

Smoke tests: что проверять, где запускать, что исключать и почему кодовые агенты снова и снова добавляют их в CI и деплой.

OpenReplay Team
OpenReplay Team
Smoke-тесты и почему агенты продолжают их писать

Smoke-тест — это небольшой набор проверок того, что развёрнутая система в принципе жива: процесс запустился, главная страница отдаёт 200, пользователь может залогиниться, а база данных отвечает на запрос. Он не оценивает корректность работы ПО; он решает, стоит ли тратить время на запуск остального набора тестов.

Если вы годами выкатывались через CI и вам ни разу не понадобилось это определение — вы не одиноки. Термин обычно возникает без предисловий, а в последнее время он возникает прямо внутри пул-реквеста: smoke.sh или smoke.spec.ts, который кодовый агент добавил, доделывая что-то другое.

В этой статье разбирается, что должно входить в smoke-тест, где он запускается, как выглядит минимальный вариант в коде, по какому фильтру отсекать лишнее и почему агенты так стабильно их создают.

Ключевые выводы

  • Smoke-тест проверяет, что развёрнутая система жива (загрузка, 200 на главной странице, логин, одно реальное чтение из базы), и занимает секунды, а не минуты.
  • Он запускается дважды: как первый гейт в CI и сразу после деплоя — против того окружения, куда реально выкатились; запуск на localhost не способен поймать сбои развёртывания.
  • Проверка попадает в smoke-набор только если её падение блокирует всех пользователей, через неё проходят все пользователи и она может сломаться при развёртывании. Если выполняется меньше трёх условий — место проверки в регрессионном наборе.
  • Кодовые агенты пишут smoke-тесты потому, что завершённой задаче нужен дешёвый, бинарный и быстрый сигнал, что ничего фундаментального не сломалось, — а это ровно то, что даёт smoke-тест.
  • Задайте жёсткий потолок для набора и переносите всё сверх него в регрессию; двенадцатиминутный smoke-набор — это регрессионный набор с неправильным названием.

Что такое smoke-тест?

Smoke-тест отвечает на один вопрос: «достаточно ли эта сборка жива, чтобы тестировать её дальше?», а его название принято возводить к включению нового железа с последующим наблюдением, не пойдёт ли дым. В софте форма та же: быстрый поверхностный проход по путям, от которых зависит каждый пользователь, с бинарным результатом и без каких-либо суждений о корректности.

Он стоит вне привычной пирамиды тестирования, а не на одном из её уровней; сами уровни разобраны в статьях Integration Tests vs End-to-End Tests и Unit vs Integration Testing in JavaScript: What to Use When. Smoke-тест — это гейт перед этими наборами, а не их участник.

Где запускается smoke-тестирование?

Smoke-тесты запускаются в двух местах: как первый гейт в CI, до старта более долгих наборов, и сразу после развёртывания — против того окружения, куда реально выкатились. Именно второй вариант размещения окупает себя, и именно его чаще всего пропускают.

Запуск smoke-теста против localhost лишает его смысла, потому что сбои, ради которых он существует, случаются только при развёртывании: отсутствующая переменная окружения, миграция, которая так и не выполнилась, бандл ассетов, который не уехал. У процесса, поднятого на CI-раннере с APP_URL=localhost и свежей in-memory базой, попросту нет таких режимов отказа. Такой запуск — это проверка загрузки, и проверки загрузки полезны, но это другая и более слабая вещь. Локальный запуск против замоканных сервисов не может упасть ни по одной из причин, по которым падает развёртывание, поэтому он не должен носить это имя.

Пост-деплойный запуск — ещё и естественный триггер для отката: AWS CodeDeploy вызывает вашу валидационную функцию, как только новая версия начинает обслуживать тестовый трафик, и её отрицательный результат инициирует откат. Но держите в голове ограниченность этого сигнала. Зелёный пост-деплойный smoke-тест доказывает, что система ответила, а не что пользовательский сценарий работает; просмотр session replay первых реальных сессий после релиза — та техника, которая разводит эти два факта, потому что кнопка оформления заказа, падающая из-за изменившегося хеша чанка, никогда не проявится в коде статуса.

Как выглядит smoke-тест?

Полноценный smoke-набор может быть одним коротким скриптом, который бьёт в реальную развёрнутую цель без моков: ограниченное по времени ожидание health-эндпоинта, один аутентифицированный запрос и одно чтение, которое проходит через приложение в базу данных.

#!/usr/bin/env bash
# smoke.sh: runs against the deployed target in $APP_URL, never localhost
set -euo pipefail

: "${APP_URL:?set APP_URL to the deployed base URL}"
: "${SMOKE_USER:?}" "${SMOKE_PASS:?}"

status() { curl --silent --output /dev/null --write-out '%{http_code}' "$@"; }

# 1. Wait for the process to come up. This absorbs container start-up, nothing else.
for _ in $(seq 1 "${SMOKE_RETRIES:-10}"); do
  [ "$(status "$APP_URL/health")" = "200" ] && break
  sleep 3
done
[ "$(status "$APP_URL/health")" = "200" ] || { echo "health: not 200"; exit 1; }

# 2. Login. Assert the one status your app returns (200 for a JSON API, 302 for a form post).
code=$(status --data-urlencode "email=$SMOKE_USER" \
              --data-urlencode "password=$SMOKE_PASS" "$APP_URL/login")
[ "$code" = "200" ] || { echo "login: got $code, expected 200"; exit 1; }

# 3. A real read through the app, with the credentials the app was deployed with.
body=$(curl --silent --fail "$APP_URL/api/products?limit=1") || { echo "query: request failed"; exit 1; }
[ -n "$body" ] || { echo "query: empty body"; exit 1; }

echo "smoke: ok"

Хелпер status использует ключ curl --write-out '%{http_code}', чтобы захватить код ответа, тогда как --output /dev/null отбрасывает тело. set -euo pipefail заставляет скрипт завершиться на первой же ошибке. Цикл повторов существует только для того, чтобы поглотить время старта после деплоя; это не способ замазать нестабильную проверку.

Каждое утверждение называет один конкретный статус. Каждый код в RFC 9110 несёт смысл, поэтому проверка, принимающая «любой ответ», — это не проверка: 404 от маршрута, который должен существовать, — это сбой развёртывания, а 302 допустим только там, где контракт предполагает редирект. Третья проверка важнее, чем кажется. Пинг сервера базы данных инструментом вроде pg_isready подтверждает, что сервер принимает соединения; чтение через приложение подтверждает, что приложение может достучаться до базы с той строкой подключения, учётными данными и схемой, с которыми оно было развёрнуто, — а это ровно тот класс сбоев, ради которого smoke-тесты и существуют.

Чему не место в smoke-тесте

Граничным случаям, бизнес-логике, всему медленному и всему нестабильному не место в smoke-тесте. Проверка попадает в smoke-набор, только если выполняются все три условия: её падение не даёт ни одному пользователю сделать вообще ничего, через неё проходят все пользователи и она может сломаться при развёртывании. Проверка, удовлетворяющая менее чем трём условиям, относится к регрессионному набору. Эта формулировка из трёх вопросов встречается как минимум в рекомендациях одного из вендоров CI-тестирования и является эвристикой, а не стандартом.

Кандидат на проверкуБлокирует всех пользователей?Проходят ли через неё все?Ломается при деплое?Вердикт
Health-эндпоинт отдаёт 200ДаДаДаSmoke
Логин с тестовой учётной записьюДаДаДаSmoke
Промокод применяет скидкуНетНетДаРегрессия
Скачивается CSV-экспорт для админаНетНетДаРегрессия
Приходит письмо сброса пароляНетНетДаРегрессия

Нестабильность дисквалифицирует сама по себе. Smoke-проверка, падающая случайным образом, приучает команду перезапускать красные гейты, а гейт, который перезапускают до тех пор, пока он не пройдёт, — больше не гейт.

Почему кодовые агенты постоянно пишут smoke-тесты?

Кодовые агенты пишут smoke-тесты, потому что агенту, завершающему задачу, нужен дешёвый, быстрый и однозначный сигнал о том, что он не сломал систему целиком, — и это ровно тот сигнал, который даёт smoke-тест. Агент, только что отредактировавший код, не может позволить себе полный набор тестов на каждой итерации и не может судить о корректности «на глаз», поэтому он тянется к проверке, которая за секунды отвечает на вопрос «оно ещё живо?» и возвращает чистый код выхода.

Это не случайность. Документация Claude Code от Anthropic советует разработчикам давать агенту что-то, что он может запустить для проверки собственной работы — будь то набор тестов, сборка, линтер или небольшой скрипт. Получив сигнал, который он способен прочитать сам, агент продолжает работать и перепроверять себя, не дожидаясь, пока ошибку заметит человек. Smoke-скрипт точно подходит под это описание — вот почему в репозиториях, написанных агентами, такой скрипт, как правило, заводится даже там, где команда никогда не пользовалась этим термином. Именно поэтому разработчики сталкиваются со «smoke-тестом» сейчас — зачастую обнаруживая его в диффе, который писали не они.

Когда такой тест появляется в PR, проверьте его по четырём вопросам. Читает ли он целевой URL из переменной окружения, а не хардкодит localhost? Утверждает ли конкретный статус вместо «не ошибка»? Завершается ли за секунды? Проходит ли каждая проверка фильтр из трёх вопросов выше? Написанный агентом smoke-тест, проваливающий любой из этих пунктов, — это либо проверка загрузки, либо регрессионный тест с неправильной этикеткой.

Режим отказа: набор разрастается

Smoke-набор перестаёт быть гейтом в тот момент, когда его время превышает несколько секунд. Схема предсказуема: каждая фича добавляет проверку «на всякий случай», агент добавляет ещё одну после каждой задачи, и однажды гейт начинает занимать двенадцать минут, а люди начинают его пропускать. С этого момента это медленный регрессионный набор под чужим именем.

Лечение — жёсткий потолок и перенос всего, что выше него, в регрессию. Наш рекомендуемый дефолт — горстка проверок, порядка пяти, укладывающихся в секунды; TestingXperts обозначает верхний предел в десять минут, после которого команды начинают пропускать гейт. Точное число менее важно, чем сам факт, что оно зафиксировано письменно и соблюдается при ревью — включая ревью правок, написанных агентами.

Заключение

Smoke-тест — это минимально возможное доказательство того, что развёртывание живо: health, логин, одно реальное чтение, запуск против того окружения, куда вы действительно выкатились, завершение за секунды. Всё остальное — регрессия. В следующий раз, когда агент подсунет вам smoke.sh, проверьте, что он нацелен на реальный URL, утверждает точные статусы и укладывается в потолок, а затем настройте его запуск после каждого деплоя.

FAQ

В чём разница между smoke-тестом и health check?

Health check — это один эндпоинт, который оркестратор или балансировщик нагрузки опрашивает, чтобы решить, направлять ли трафик или перезапустить контейнер; в Kubernetes это liveness- и readiness-пробы. Smoke-тест выполняется один раз за развёртывание, вызывает этот эндпоинт плюс логин и чтение из базы и возвращает код выхода, который открывает или закрывает пайплайн. Health-эндпоинт — это первое утверждение smoke-теста, а не его замена.

В чём разница между smoke-тестированием и sanity-тестированием?

Smoke-тестирование широкое и поверхностное: оно проверяет, что ключевые пути сборки живы, до начала более глубокого тестирования. Sanity-тестирование, в общепринятой QA-терминологии, узкое и глубокое: оно проверяет, что конкретное исправление или изменение работает на сборке, прошедшей smoke, и часто считается подмножеством регрессионного тестирования. В CI-пайплайне smoke-набор — это гейт; sanity-проверкам место вместе с регрессией.

Стоит ли запускать smoke-тесты на продакшене и безопасно ли это?

Да. Пост-деплойный запуск должен быть нацелен на то окружение, до которого реально доходят пользователи, включая продакшен, потому что сбои развёртывания проявляются только там. Обеспечьте безопасность: используйте выделенную заранее созданную тестовую учётную запись, передаваемую через CI secrets, ограничьте проверки логином и запросами только на чтение и исключите всё, что пишет данные или отправляет письма. Если запись неизбежна, ограничьте её тестовым тенантом и подчистите за собой в том же скрипте.

Можно ли написать smoke-тест на Playwright или Cypress вместо shell-скрипта?

Да, при условии соблюдения тех же правил: читать базовый URL из переменной окружения, утверждать точные статусы или видимые элементы и завершаться за секунды. Браузерные раннеры добавляют время старта и загрузки страницы на каждую проверку, поэтому ограничьте браузерный smoke-набор одним-двумя сценариями, а HTTP-проверки оставьте в curl. Вынесите smoke-спеку в отдельный файл, чтобы CI мог запускать её без остального набора.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.