| name | component-tests |
| description | Генерация, ревью и расширение компонентных тестов программы — сетевого сервиса (OpenAPI/AsyncAPI) или CLI-инструмента (api-specification/cli.md + report.schema.json) — как чёрного ящика. Применять, когда нужно создать новые .feature, дополнить существующие, добавить тестовые данные/фикстуры или оценить набор на полноту покрытия контракта и режимов отказа. Не применять для юнит-тестов или контрактных тестов между сервисами. |
Компонентные тесты — спецификация чёрного ящика
Skill для генерации компонентных тестов программы — сетевого сервиса или
CLI-инструмента. Тест — это исполняемая спецификация: что программа обещает
делать на штатном пути и при отказе каждой внешней связи. Не охота за багами
и не покрытие кода.
Объект — спецификация программы, не привязка к инструментам
Тестируем программу как чёрный ящик в изоляции — сетевой сервис или
CLI-инструмент, безразлично. Это общий инженерный подход, не зависящий от языка
и фреймворка: задача одна — проверить спецификацию интерфейса программы
(контракт), а не её внутренности. Для сервиса контракт — OpenAPI/AsyncAPI и
внешние входы это эндпоинты/события; для CLI-тула контракт — cli.md +
report.schema.json, а внешние входы — подкоманды.
Изоляция реализуется единообразно: вся среда поднимается в Docker Compose.
Для сервиса — сам сервис плюс его зависимости. Для CLI-тула — бинарь в контейнере
(запускается как одноразовый сервис compose против фикстур) плюс его внешние
зависимости как сервисы compose, включая заглушку внешнего API (например,
LLM-эндпоинта), к которой тул обращается по имени сервиса. Заглушка — полноценный
сервис, отвечающий по реальному протоколу, а не in-code мок.
Выбор среды не пересматриваем: компонентные тесты — всегда в Docker Compose.
Принцип: контракт первичен
Компонентные тесты пишутся до кода сервиса (TDD-цикл). Значит, режимы
отказа интеграций должны быть зафиксированы в контрактах, не выведены
из кода адаптеров. Код — следствие контракта, не источник.
Источники режимов отказа в порядке приоритета:
-
OpenAPI (api-specification/openapi.yaml) — для синхронных эндпоинтов.
Все 5xx-ответы с разбиением по error.code.
-
README раздел «Карта режимов отказа» — для асинхронных интеграций
и любых интеграций, не описанных через коды HTTP.
-
Fallback: код адаптера — если контракт ещё не детализирован.
Создать TODO в backlog.md: «зафиксировать режимы отказа в контракте».
Граница со слоем юнитов
Компонент-тесты проверяют обещания интерфейса наружу: коды возврата,
поля отчёта по схеме, реакцию на каждый различимый режим отказа интеграции.
Они не воспроизводят бизнес-логику — её доказывают юниты в internal/.
Перед тем как добавить фикстуру или сценарий, ответь:
Какую новую ветку контракта он триггерит — новый error.code, новое
значение exit, новое значение layers.Ln.status / jtbd.*.status,
новое поле отчёта, новый формат вывода, новый флаг CLI?
Если ни одну — это юнит-уровень, не добавляй. «Покрыть L1-логику глубже»
или «больше плохих ссылок» не считается: контракт не различает «одна битая
ссылка» и «десять», он различает «есть blocker / нет blocker». Для каждой
ветки контракта достаточно минимальной фикстуры, которая её триггерит.
Лишние сценарии — удалять. Если ревью показывает, что сценарий не
триггерит новую ветку контракта (дубль, юнит-уровень, «на всякий случай»)
— его нужно удалить, а не оставить «про запас». Число сценариев должно
точно соответствовать формуле: N = N_подкоманд + Σ режимы_отказа. Сценарии
сверх формулы — признак неточного понимания границы со слоем юнитов.
Модель фикстур: изолированные на слайс
Каждый слайс имеет собственный набор фикстур — repo-good-<slice> и
repo-bad-<slice>. Фикстуры между слайсами не разделяются.
Почему не shared-фикстуры. Разделяемая repo-good накапливает инвариант
всех зарегистрированных check-типов одновременно: добавился новый слайс →
старая фикстура может перестать быть «хорошей» для нового check-а. Это
нарушает независимость слайсов и создаёт скрытую связность: изменение в
фикстуре одного слайса ломает тесты другого. Изолированные фикстуры этого
лишены.
Исключение — assess. Подкоманда assess запускает все слои сразу,
поэтому её фикстура (repo-good-assess, repo-bad-assess) по определению
покрывает все check-типы. Но это отдельная фикстура, спроектированная
специально для assess, а не переиспользование фикстур других слайсов.
Минимальность фикстуры. Фикстура каждого слайса содержит ровно то,
что нужно для триггера одной ветки контракта, — не больше. Лишний контент
в repo-bad-<slice> делает failure-сценарий нечитаемым: непонятно, какой
именно check сработал.
Проектирование фикстуры нового слайса
Перед написанием .feature — спроектируй фикстуру:
repo-good-<slice>: минимальный репозиторий, в котором новый check
не производит нарушений. Только тот контент, который нужен для happy-path.
Никакого «реалистичного» наполнения сверх необходимого.
repo-bad-<slice>: добавь к minimal-базе ровно один элемент,
триггерящий нужную ветку отказа. Один элемент — одна ветка.
- Для каждой дополнительной ветки отказа — отдельный вариант
repo-bad
или отдельный параметр (если степы это поддерживают).
Этот шаг выполняется до написания сценариев и до кода — он часть
дизайна, не отладки реализации.
Формула
N_тестов = N_эндпоинтов_API + Σ (число различимых режимов отказа интеграции i)
-
N_эндпоинтов_API — количество эндпоинтов в OpenAPI плюс количество
событий (publish и subscribe) в AsyncAPI; для CLI-тула — количество подкоманд.
-
Различимый режим отказа — каждый отдельный тип ошибки, который сервис
обещает наружу для данной интеграции.
Что считать интеграцией
Интеграция — это выход за процесс сервиса:
- сеть (HTTP, gRPC, очереди, БД через драйвер);
- файловая система;
- IPC (Inter-Process Communication, межпроцессное взаимодействие).
Локальные библиотеки в памяти процесса — модули, не интеграции.
Алгоритм генерации тестов
Шаг 0. Спроектировать режимы отказа и подготовить раннер
(если ещё не сделано).
Перед тем как писать .feature, должны существовать:
- 5xx-ответы с
error.code в OpenAPI на каждый различимый режим отказа.
- Таблица «Карта режимов отказа» в README с тем же набором.
- Раннер компонентных тестов (godog/cucumber/...) с docker-compose,
набором базовых степов (HTTP, доменная авторизация, фикстуры
режимов отказа интеграций) и хотя бы одним smoke-сценарием.
Без любого из трёх Шаг 1 проваливается: либо нечего проверять,
либо нечем проверять.
Промпт для агента (лаконичный, самодостаточный):
Прочитай README раздел «Стек» и определи интеграции сервиса
(правило: интеграция = выход за процесс — сеть, ФС, IPC).
Для каждой интеграции перечисли теоретически возможные режимы
отказа. Объедини в один error.code те режимы, на которые
клиент и ops реагируют одинаково; разнеси по разным
кодам те, у которых поведение различается (HTTP-код,
Retry-After, эскалация оператору). Зафиксируй результат:
в OpenAPI — 5xx-ответы с примерами error.code для каждого
различимого режима (разные HTTP-коды — разные responses);
в README — таблица «Карта режимов отказа» с теми же строками
и колонкой «поведение клиента». Решение «почему именно столько
режимов» — в docs/adr/.
Правило различения: режим — отдельный, если хотя бы одно из четырёх
различается между ним и другими режимами интеграции:
- HTTP-статус (503 vs 507 vs 500);
- заголовки ответа (
Retry-After есть/нет);
- действие клиента (ретраить / не ретраить / ретраить с backoff);
- действие оператора (ничего / alert / эскалация).
Если все четыре одинаковы — режимы сворачиваются в один error.code.
Минимум, что должно остаться после Шага 0:
- секция
components/responses в OpenAPI содержит общий ответ
на отказ интеграции (например, ServiceUnavailable) с примером
error.code для каждого различимого режима;
- каждый эндпоинт в
responses ссылается на этот общий ответ;
- в README таблица «Карта режимов отказа» содержит ровно те же
строки, что и
error.code-примеры в OpenAPI.
Шаг 1. Прочитать api-specification/openapi.yaml. Перечислить эндпоинты.
Если есть asyncapi.yaml — перечислить события.
Шаг 2. На каждый эндпоинт и каждое событие — один happy-path сценарий.
Не склеивать фазы двухфазного протокола (например, регистрация:
challenge + attestation) в один сценарий — это два отдельных контракта.
Шаг 3. Определить режимы отказа из контрактов:
- синхронные: все 5xx из OpenAPI с разбивкой по
error.code;
- асинхронные: строки таблицы «Карта режимов отказа» из README;
- fallback: код адаптера + TODO в
backlog.md.
Шаг 4. На каждый режим отказа — один сценарий.
Шаг 5. Сверить: N = N_эндпоинтов + N_событий + Σ режимы отказа.
Ревью и расширение существующих тестов
Когда задача — не «написать с нуля», а «доработать тестовые данные / тесты»
или «оценить покрытие», направление обхода инвертируется: ходи от
контракта вниз к данным, не от данных вверх к гипотезам «чего бы ещё добавить».
Алгоритм ревью:
- Выпиши пункты контракта — подкоманды/эндпоинты, поля отчёта по схеме,
значения
exit, флаги CLI, строки «Карты режимов отказа».
- Для каждого пункта найди сценарий, который его триггерит. Если нет —
это пробел, кандидат на новый сценарий или фикстуру.
- Для каждого существующего сценария примени диагностический вопрос из
«Границы со слоем юнитов». Если он не покрывает новую ветку контракта —
он либо дубль, либо юнит-уровень, либо ложно-полезная «отладочная»
ассерция (например, проверка конкретного
violation.code вместо
severity=blocker).
Типовой источник дрейфа на ревью — мышление «больше данных = лучше». Больше
данных полезно только если они открывают новую ветку контракта. Узкие
фикстуры «под один слой» имеют смысл, только если контракт обещает что-то,
что широкая фикстура триггернуть не может (например, assess --up-to со
status="skipped" требует фикстуру, ломающую ранний слой и не ломающую
поздний — это контрактная необходимость, не желание глубины).
Структура файлов
- Один
.feature файл на ресурс API.
- Имя файла = имя ресурса в нижнем регистре.
- Расположение:
component-tests/<resource>.feature.
Что НЕ делать
- Не выводить режимы отказа из кода как основной путь.
- Не генерировать сценарии валидации полей запроса (юнит-уровень).
- Не повторять бизнес-логику (доказана юнитами) — см. «Граница со слоем юнитов».
- Не писать smoke-test на запуск (контролирует компилятор).
- Не добавлять сценарии «на всякий случай».
- Не склеивать фазы протокола в один сценарий.
Запуск
Все компонентные тесты запускаются в Docker Compose с реальными зависимостями —
и для сервиса, и для CLI-тула (бинарь в контейнере как сервис compose). Тест
дёргает настоящий интерфейс программы: HTTP-эндпоинт сервиса или подкоманду тула.
Внешний API замещается заглушкой-сервисом в том же compose, отвечающей по
реальному протоколу. Никаких in-code моков.
Чек-лист перед коммитом
- Шаг 0 пройден: интеграции и режимы отказа зафиксированы в OpenAPI и README.
- Карта режимов отказа в README заполнена и актуальна.
- Число
.feature файлов = число ресурсов в API.
- Число happy-path сценариев = число эндпоинтов + число событий.
- Число сценариев отказа = сумма режимов отказа из OpenAPI и README.
- Все сценарии запускаются в Docker Compose с реальными зависимостями.
- Нет валидации полей, бизнес-логики, smoke-тестов.
- Фикстуры изолированы на слайс —
repo-good-<slice> и repo-bad-<slice>.
Фикстуры других слайсов не используются. Фикстура assess — отдельная.
- Число сценариев = формула — лишние удалены; каждый сценарий триггерит
ровно одну ветку контракта, которой без него не было.
Чек-лист шаблона (Шаг 0) перед хендоффом другому агенту
Дополнительно к чек-листу выше — если на этом шаге готовится шаблон
тестов (раннер, базовые степы, compose), который потом получит другой
агент или разработчик для написания .feature-файлов:
- Каждый степ из
steps/*.go вызван хотя бы раз из features/smoke.feature.
Не из Go-кода (макрошага), а именно из Gherkin — там другая семантика
аргументов и парсинга. Степы, написанные «в голове через Go-вызов»,
часто проваливаются при первой же попытке вызвать их из .feature
(например, динамический id, который в Gherkin-строке статически не
зафиксировать).
- Smoke зелёный — full pipeline (
docker compose build → up → run).
- README шаблона перечисляет все степы с их регулярками. Если степ
нельзя выразить одной фразой — это сигнал переименовать или разнести.
- Включены степы для типовых случаев API: точное равенство JSON-поля,
присутствие поля, непустое поле (для динамических значений вроде
токенов и timestamp). Без них первый же endpoint с JWT упрётся в стену.
Пример: passkey-demo-api
- 6 эндпоинтов в OpenAPI (двухфазная регистрация, двухфазный вход,
выход,
/users/me).
- 1 интеграция — SQLite.
- 2 различимых режима отказа SQLite:
db_locked (503 + Retry-After) — SQLITE_BUSY, lock contention;
db_disk_full (507) — диск переполнен на запись.
- AsyncAPI нет.
N_тестов = 6 + 2 = 8. Восемь компонентных сценариев — полная
спецификация сервиса как чёрного ящика.
Замечание: db_unavailable намеренно не выделен — для встроенной
SQLite это синтетический режим. Если бы интеграция была сетевой
(Postgres, MySQL) — добавили бы. Принцип: в спеку идут только
реально воспроизводимые в тестах режимы, не теоретические.