9 мая 2026
BDD помогает участникам разработки договориться об ожидаемом поведении системы на конкретных примерах. Сценарии Gherkin можно использовать и как контекст для агента, и как основу автоматизированных проверок. В статье разбираю discovery, formulation и automation, устройство сценариев и проверку соответствия реализации договоренностям.
Тезис: BDD - формат, в котором PM, инженер и агент читают одно и то же
BDD появился задолго до AI-агентов. Дэн Норт в статье 2006 года «Introducing BDD» переписал TDD в терминах поведения, потому что слово «тест» сбивало команды с толку: люди думали, что пишут тесты, на самом деле они описывали ожидаемое поведение системы. Cucumber и Gherkin закрепили формат: текст, который читает stakeholder, и тот же текст, который исполняет тестовый раннер.
Сам формат намеренно прост. В Gherkin около десяти ключевых слов, и они образуют декларативную грамматику с фиксированным порядком:
| Ключевое слово | Назначение |
|---|---|
Feature: | Заголовок документа, одна на файл |
Background: | Общие предусловия, выполняются перед каждым сценарием в файле |
Rule: | Группировка сценариев под общим бизнес-правилом (Gherkin 6+) |
Scenario: или Example: | Один тест-кейс |
Scenario Outline: + Examples: | Параметризованный сценарий с таблицей данных |
Given | Исходное состояние («система в таком-то состоянии») |
When | Событие или действие («пользователь делает X») |
Then | Ожидаемое наблюдаемое последствие |
And, But | Продолжение цепочки шагов того же типа |
* | Звездочка как замена любому шаговому ключевому слову (для bullet-стиля) |
@-теги | Метаданные сценария (приоритет, scope, owner, контрибутирующий спек) |
# ... | Комментарии |
"""...""" или ```...``` | Doc strings - multi-line аргумент шага (str) |
| a | b | | Data tables - табличный аргумент шага |
Короткие Gherkin-сценарии помогают обсуждать примеры поведения с участниками разработки. Однако понятность зависит от языка предметной области и качества примеров; чтение и написание сценариев может требовать совместной практики.
При этом грамматика составная. Background + Scenario Outline + Examples + теги + doc strings + data tables в одном файле дают достаточную выразительность для любого слоя acceptance-тестирования: pure-unit-стиль (один Given, один When, один Then), интеграционный стиль (multi-step Given для setup, табличные Examples для матрицы кейсов), end-to-end стиль (Background для общего контекста + цепочка When/Then через нескольких акторов или этапов - спорная практика, см. ниже).
Канонический минимальный сценарий выглядит так:
Feature: Magic-link sign-in
Scenario: User signs in with a fresh magic link
Given a registered user "alice@example.com"
When the user requests a magic link
And the user opens the link within 15 minutes
Then the user is signed in
В большом проекте структура раскладывается на несколько уровней. Один .feature-файл соответствует одному «slice» функциональности (например, magic-link sign-in или csv-export). Один каталог - одной capability (auth, agents, tools, ...). Все капабилити лежат в общем top-level дереве features/. Внутри файла:
- Feature-level теги в первой строке после комментариев - описывают капабилити и контрибутирующие спеки;
Feature:+As a/I want/So that- продуктовое описание для stakeholder;Background:- общие предусловия;- N сценариев, каждый с собственными тегами (scope, priority, spec-link), Given/When/Then-цепочкой.
Полный пример - закрытая регистрация (invite-only mode) с шапкой, feature-level тегами и тремя сценариями:
# Capability: auth
# Contributing specs:
# - 008-close-registration (introduces the kill-switch)
# - reference/auth-flows (cross-cutting auth invariants)
@capability:auth @spec:008 @spec:reference-auth-flows
Feature: Closed registration (waitlist mode)
As a product team operating in invite-only mode
I want public signup blocked with a clear waitlist response
So that we can flip the registration kill-switch in under 5 minutes
Background:
Given a clean tenant for this scenario
@spec:008 @user-story:008-1 @smoke @p1
Scenario: Landing CTA shows waitlist label when registration is closed
Given the registration kill-switch is on
When an anonymous visitor opens the landing page
Then the primary CTA reads "Join the waitlist"
@spec:008 @user-story:008-3 @integration @p1
Scenario: New email signup returns 503 with registration_closed error
Given the registration kill-switch is on
When a new email signup is attempted with email "bob@example.com"
Then the response is 503 with body field error="registration_closed"
And no magic-link token is created
And no email is sent
@spec:008 @user-story:008-2 @smoke @p1
Scenario: Existing user's magic link still works when registration is closed
Given the registration kill-switch is on
And a registered user "alice@example.com"
When the user requests a magic link
Then the user receives an email with a one-time link
And the user can sign in with that link
Те же ключевые слова работают на любом уровне - от одного inline-теста до 40-сценарийной capability с feature-level Background. Это и есть «композиция»: формат не растет сложнее по мере роста проекта; добавляются только количество файлов, тегов и капабилити.
Один нюанс, на котором BDD-сообщество расходится. Канонический Gherkin предписывает один When на сценарий: «Describe an initial context (Given steps), describe an event (When steps), describe an expected outcome (Then steps)». Жесткая интерпретация - «one scenario, one behavior» - отстаивается, например, Andrew Knight (Automation Panda); по его терминологии это «cardinal rule of BDD», а длинные When/Then цепочки в imperative UI-сценариях («open browser - click - check - click - check») - явный антипаттерн.
Liz Keogh, одна из учеников Дэна Норта и активных BDD-практиков, явно опровергает жесткое правило: бывают сценарии, в которых смысл - именно в последовательности нескольких событий или взаимодействии нескольких акторов. Ее каноничный пример из «On multiple Givens, Whens, and Thens»:
Given Clare is editing trade 12345
When Stephen edits and saves that trade
And Clare tries to save that trade
Then she should be told that Stephen has already edited it.
Здесь два разных When отражают двух акторов; смысл сценария - в их временном чередовании, а не в одном изолированном событии. Сводя такой сценарий к одному When, теряется контракт - именно конкуренция и порядок действий проверяются.
Безопасный baseline: один When на сценарий, как рекомендует Cucumber-документация и большинство руководств по «better Gherkin». Цепочка When/Then используется как осознанное исключение в трех контекстах - end-to-end-сценарии (по терминологии Knight это допустимое исключение), race-condition-проверки (Liz Keogh), API-flow с несколькими цепными вызовами. Если сценарий распадается на «click → check → click → check → click → check» - это сигнал, что его стоит разбить на несколько отдельных Scenario:.
Те же восемь-десять строк сценария работают как PRD для PM, контракт для QA и исполняемый тест для CI. Когда между PM и кодом появляется AI-агент, файл начинает работать еще в одном качестве - как контекст, который определяет границы сгенерированного кода.
Причина - в том, как агент работает с контекстом. Агент не помнит вчерашнего разговора между PM и инженером. Он не догадывается, что фраза «онбординг» в задаче означает «регистрация + welcome-email + первичная настройка профиля». Он работает с тем, что записано в репозитории. Если запись «как должно быть» (PRD в Confluence) и запись «как проверяется» (тесты в репо) - два разных файла на двух разных платформах, агент будет держать в контексте одну из них и игнорировать другую. Дрейф между ними агент не увидит, потому что он не делает Confluence cross-check на каждый запрос.
Gherkin-файл, лежащий в репозитории, читаемый PM и исполняемый pytest-bdd, делает эту проблему ненаблюдаемой по построению: один источник, одна проверка, один артефакт в Pull Request.
Три практики BDD по cucumber.io: discovery, formulation, automation
Cucumber-документация разбирает BDD на три практики:
- Discovery - «what it could do». Структурированные разговоры (discovery workshops, example mapping) на реальных примерах, чтобы вырастить общее понимание потребностей пользователя, правил системы и границ работы.
- Formulation - «what it should do». Запись примеров в виде структурированной документации, читаемой и человеком, и компьютером, для подтверждения общего понимания до начала имплементации.
- Automation - «what it actually does». Использование тех же примеров как тестов, направляющих разработку: тест падает до того, как поведение реализовано.
Когда в команде появляется агент, эти три практики не отменяются - они перераспределяются.
Discovery остается за людьми
Discovery - это набор практик про разговор между бизнесом и инженерами на конкретных примерах. Cucumber-проект и BDD-канон называют три устоявшихся формата:
- Three amigos meeting - встреча трех ролей: PM (или business analyst), инженер, QA. Каждая роль приносит свой угол: PM знает intent, инженер знает constraints, QA знает граничные случаи. Цель - найти расхождения в понимании до того, как они станут багами. Длительность 30-60 минут на одну user story.
-
Example mapping (Burton, 2015; canonical Cucumber-практика) - структурированная сессия на 25-30 минут вокруг одной user story. Используются карточки четырех цветов:
- желтая - сама user story сверху карты;
- синие - acceptance criteria (бизнес-правила, которые должны выполняться);
- зеленые - конкретные примеры, иллюстрирующие каждое правило;
- красные - открытые вопросы, на которые в этой сессии нет ответа.
- Specification by example (Adzic, 2011) - общее название для практики, в которой acceptance criteria формулируются через конкретные примеры, а не абстрактные требования. Example mapping - одна из конкретных техник под этим зонтиком.
Ожидаемый артефакт discovery-сессии - не Gherkin (это уже formulation, следующий шаг). Артефакт - это структурированная заметка с тремя секциями:
- Rules - бизнес-правила (синие карточки), пронумерованные;
- Examples - конкретные иллюстрации каждого правила (зеленые карточки), сгруппированные под соответствующим rule;
- Open questions - перечень нерешенных вопросов с указанием owner-а, который должен принести ответ.
В большинстве команд этот артефакт фиксируется в Notion / Miro / Confluence-странице или в issue-трекере как комментарии к story. Формальный шаблон не критичен; критичны три секции - правила, примеры, вопросы.
Этот артефакт затем превращается в Gherkin на стадии formulation: каждое бизнес-правило (синяя карточка) часто становится группой сценариев в .feature-файле; каждый конкретный пример (зеленая карточка) - отдельным Scenario: или строкой в Examples: у Scenario Outline.
Агент в discovery не участник:
- он не оценивает regulatory constraint;
- не видел support-тикеты за последний квартал;
- не знает, что часть клиентов не справляется на конкретном шаге;
- не предлагает edge-cases на основе personal experience с клиентами.
На этапе discovery участники уточняют задачу и ожидаемое поведение. Агент может записывать договоренности, задавать вопросы и предлагать варианты сценариев. Проверять бизнес-правила и принимать решения должны люди, которые отвечают за продукт.
Formulation становится human-AI циклом
После discovery остается структурированная заметка: rules + examples + open questions. Formulation превращает ее в Gherkin - бизнес-правила группируют сценарии, конкретные примеры становятся отдельными Scenario: или строками в Examples:-таблице у Scenario Outline. Это формальная переработка артефакта в исполняемый контракт.
Шаги цикла, когда в команде есть агент:
-
Человек инициирует scaffolding. В spec-driven workflow это команда вроде
/speckit-specify "users can sign in with their Apple ID". Скилл пишетspec.mdна основе discovery-артефакта, сканирует его на capability cues («sign in» →auth), выбирает целевой путьfeatures/auth/apple-signin.feature, вызывает scaffolder. -
Scaffolder создает скелет. На выходе -
.feature-файл с обязательной шапкой (# Capability:,# Contributing specs:), feature-level тегами,Background:, и однимScenario:на каждый acceptance summary изspec.md. Шаги пока шаблонные:Given [initial state],When [action],Then [expected outcome]- это явные TODO, которые нужно заполнить. -
Агент достраивает черновик. Имея на руках discovery-артефакт (rules + examples) и шаблон сценария, агент заполняет конкретные Given/When/Then через бизнес-язык. Здесь агент эффективен по трем причинам: формальный язык с фиксированной грамматикой, правила именования (placeholder в кавычках, lowercase бизнес-термины), переиспользуемые шаги из существующих файлов в той же капабилити (агент сканирует
tests/bdd/steps/auth_steps.pyи предлагает шаги, которые уже забиндены). -
Человек ревьюит контракт. Это core-контрольная точка фазы. Что проверяется:
- Уровень абстракции. Шаги пишутся бизнес-языком (
When the user requests a magic link), а не именами внутренних API (When messaging_dispatcher.handle returns ok). Если агент сполз в имплементационные детали - возвращается на доработку. - Покрытие правил. Каждое синее правило из discovery-артефакта должно быть представлено хотя бы одним сценарием. Если агент сгенерировал 5 сценариев на 7 правил - проверить, какие два пропущены и почему. Часто пропускают правила, которые сформулированы как negative requirement («система не должна X»).
- Пограничные случаи. В сценариях нужно проверить ошибки, истекшие токены, недоступность зависимостей и одновременные запросы. Агент может помочь составить список, но его нужно сверить с рисками конкретной задачи и договоренностями discovery.
- Теги. Каждый сценарий несет scope-тег (
@smoke/@integration/@e2e), priority-тег (@p1/@p2/@p3),@spec:<id>и@user-story:<id>-<n>. Если scope или priority не очевидны из контекста - агент часто проставляет дефолт@smoke @p1; человек корректирует, ориентируясь на нагрузочный профиль (требует ли сценарий DB / Redis -@integration; полный pipeline -@e2e). - End-user текст verbatim. Если в продукте есть user-facing strings (bot-сообщения, email subjects, UI labels), они должны попадать в шаги дословно, в кавычках, как данные. Агент иногда переводит / перефразирует / транслитерирует их - возвращается на дословное цитирование.
- Структура When/Then. Если агент сгенерировал длинную
When → Then → When → Thenцепочку - проверить, действительно ли смысл сценария в этой последовательности (как у Liz Keogh с двумя акторами), или это императивный UI-скрипт, замаскированный под BDD-сценарий, и его надо разбить на несколько отдельныхScenario:.
- Уровень абстракции. Шаги пишутся бизнес-языком (
-
Итерация. Если ревью нашел проблемы, человек указывает агенту конкретные правки: «у Scenario 3 шаг 2 - на уровне реализации, переформулируй»; «добавь сценарий для concurrent edit (rule 4)»; «помечай
@e2eвместо@smoke, требуется DB и Redis». Агент применяет, человек ревьюит снова. На capability средней сложности 1-3 итерации обычно достаточно. -
Финал:
## Feature files touched. После approve скилл дописывает вspec.mdсекцию со ссылкой наfeatures/auth/apple-signin.featureи закрывает фазу formulation. Спек переходит на/speckit-planс прогономvalidate_drift --spec NNN.
Что отличает этот цикл от прямой генерации Gherkin агентом без human-контрольная точка - именно review-стадия. Discovery-артефакт - источник правды, против которого ревьюер сверяет агентский draft. Без него ревьюер сверяет «звучит ли разумно», и LLM, оптимизированный под звучать разумно, проходит проверку с любым контентом.
Automation - территория агента
Step bindings - это Python-функции с декораторами @given / @when / @then. Они работают как обычный pytest-код: используют существующие service-фикстуры (db_session, identity_app, recorded_llm), вызывают production-код, проверяют результаты.
Чтобы заполнять bindings, нужно знать, какие шаги уже забиндены, а какие появились в новом сценарии и требуют новой функции. Эта информация извлекается двумя способами:
-
Pytest collection.
pytest --collect-onlyдля BDD-сьюта проходит все .feature-файлы и пытается зарезолвить каждый шаг в существующий binding. Когда binding не найден, pytest-bdd бросаетStepDefinitionNotFoundErrorс указанием конкретного шага и файла. Это динамический механизм - он отвечает на вопрос «что не забиндено прямо сейчас» точно, без эвристики. -
Static AST scan. Скрипт обходит
tests/bdd/steps/*.py, собирает все вызовы@given(...)/@when(...)/@then(...)через AST-парсер, извлекает паттерны декораторов (plain string илиparsers.parseargument). На выходе - список «вот все pattern-ы, к которым уже привязан код, и в каком файле / функции». Это статический механизм - он отвечает на вопрос «что у нас в принципе есть» без запуска тестов.
Комбинация этих двух механизмов дает полную картину: список бинденных шагов (AST-скан) минус шаги в новом сценарии (collection failure) = шаги, которые надо написать.
На основе этой картины spec-driven скилл (например, /speckit-tasks) формирует задачи. Для каждого нового сценария он эмитит одну [BDD] wiring-task, в которой каждый шаг помечен либо «reuse» (если найден в AST-скане - с указанием конкретного файла и функции), либо «NEW» (если не найден):
- [ ] T010 [US1] [BDD] Wire scenario "User signs in with valid Apple ID"
in features/auth/apple-signin.feature (@spec:017 @user-story:017-1).
Steps to bind:
- Given an Apple ID with email "{email}" → NEW: add to tests/bdd/steps/auth_steps.py
- And a clean tenant → reuse tests/bdd/steps/tenant_steps.py:given_clean_tenant
- When the user clicks "Sign in with Apple" → NEW: add to tests/bdd/steps/auth_steps.py
- Then a JWT is issued with the user's email claim → NEW: add to tests/bdd/steps/auth_steps.py
Acceptance: pytest tests/bdd/features/test_auth.py -k "User signs in with valid Apple ID" is green.
Этот формат - готовый промт для агента. Reuse-шаги дают сигнатуру существующего binding-а, который агент должен вызывать (или подсмотреть в нем паттерн). NEW-шаги дают конкретный файл, в который надо добавить новую функцию. Агент пишет код, прогоняет pytest -k "...", видит зеленый сценарий - закрывает task. Discovery остался за человеком, formulation прошел human review, automation идет по детерминированному pipeline.
Drift как корневая проблема, которую BDD решает заранее
Один из дизайн-выборов BDD: вынести спецификацию и проверку в один файл вместо нескольких артефактов в разных системах. Это инженерное решение - не drama про «дрейфующие источники правды», а просто способ построить процесс так, чтобы дрейф между PRD, тестами и кодом не возникал по построению. Если acceptance criteria и acceptance test - один файл, между ними нет промежуточного шага «синхронизировать», на котором все ломается.
Это дает три полезных свойства:
- Stakeholder читает контракт без перевода. PM, спрашивающий «что продукт делает сегодня», открывает
features/<capability>/<name>.featureи читает прозу. Не нужно ходить в репозиторий за тестами и не нужно звать инженера для перевода. - Спецификация и тест меняются одним PR. Когда поведение меняется, меняется один файл. Сценарий не становится «историческим документом» - он лежит там, где идут изменения.
- Acceptance scenarios не дублируются. Не нужно писать отдельный набор Python-тестов, которые перекодируют acceptance scenarios из
spec.mdв test-language - сценарий уже исполняемый.
В работе с агентом это особенно полезно. Агент работает в цикле «прочитал контекст - сгенерировал patch - запустил тесты». Если контекст и тесты - один файл, цикл замкнут: «зеленые тесты» и «контракт выполняется» означают одно и то же. Если контекст лежит в Confluence, а тесты в репозитории, цикл размыкается - агент будет генерировать patch, который проходит тесты, но не обязательно соответствует контракту в Confluence, и не заметит этого.
Insight: один артефакт, два потребителя
Принцип, на который сходится Cucumber-документация и большинство практических реализаций: артефакт, который читает stakeholder, чтобы понять текущее поведение, и артефакт, который исполняет тестовый раннер, чтобы это поведение проверить, должны быть физически одним файлом. Если они разойдутся, drift вернется.
«Физически одним» - не метафора. Один файл features/auth/closed-registration.feature лежит в репозитории. PM открывает его на GitHub и читает прозу. CI запускает pytest, который через pytest-bdd парсит тот же файл и исполняет шаги. PR-ревьюер видит diff на тот же файл, когда меняется поведение. Других мест нет.
Это накладывает ограничения на авторов:
- Шаги пишутся бизнес-языком, а не именами внутренних API. «User receives a confirmation», не «messaging_dispatcher.handle returns ok».
- End-user контент (например, русские строки в bot-сообщениях) сохраняется внутри step arguments дословно, потому что это user-facing контракт, а не данные теста.
- Step bindings - переводчик с бизнес-языка на runtime: они принимают Gherkin-текст и вызывают production-код.
Что это меняет для агента:
- Агент, читая файл, получает контекст, который читает PM. Не «как у нас устроена аутентификация», а «что пользователь делает и что система отвечает».
- Агент, дописывая фичу, обязан изменить тот же файл, который читает PM на следующий ревью. Не два файла, не четыре - один.
- Когда агент гонит pytest, он гонит ровно те сценарии, которые PM считает контрактом. «Зеленые тесты» и «контракт выполняется» становятся синонимами.
Это не отменяет человеческое ревью - discovery остается за людьми, как обсуждалось выше. Но все, что между discovery и production-кодом, сводится в один файл.
Опорная архитектура BDD-стека
На уровне дерева репозитория Python-проекта типичный BDD-стек выглядит так:
features/ # current-state контракты, top-level
├── auth/
│ ├── magic-link.feature
│ ├── webauthn.feature
│ ├── jwt-issuance.feature
│ └── closed-registration.feature
├── agents/
│ ├── agent-loop.feature
│ └── agent-tuning.feature
├── tools/
│ ├── doc-spec-matching.feature
│ ├── csv-export.feature
│ └── ...
├── platform/
│ ├── state-engine.feature
│ └── ...
└── ...
tests/bdd/ # step bindings + execution glue
├── conftest.py # shared fixtures, tag → marker, V-TAG enforcement
├── steps/
│ ├── world.py # World dataclass + per-scenario fixture
│ ├── tenant_steps.py
│ ├── agent_steps.py
│ ├── fsm_steps.py
│ ├── tool_steps.py
│ ├── channel_steps.py
│ ├── llm_steps.py
│ └── bdd_meta_steps.py
└── features/
├── conftest.py
├── test_auth.py # 5 строк: scenarios("auth")
├── test_agents.py
└── ...
tools/bdd/ # production-grade тулинг
├── parser.py # Gherkin AST → typed dataclasses
├── render.py # walks features/ → manifest.json + markdown
├── scaffold.py # creates / amends .feature files
└── validate_drift.py # three-direction drift validator
specs/ # decision history (immutable once shipped)
├── 001-initial-feature/
│ ├── spec.md # contains "Feature files touched" section
│ ├── plan.md
│ └── tasks.md
└── ...
В этом примере features/ физически не лежит под specs/. Это два разных дерева с разной семантикой:
| Дерево | Роль | Изменчивость | Аудитория |
|---|---|---|---|
specs/ |
Decision history - почему изменение сделано, когда, что решено | Append-mostly; spec-и не меняют после shipping | Инженеры, читающие историю проекта |
features/ |
Actual current state - что система делает сегодня, по capability | Меняется в том же PR, что и поведение | PM, QA, инженеры |
Жесткое правило (V-LOCATION): .feature-файл не имеет права лежать где-либо под specs/. Связь двусторонняя plain-text:
specs/<dir>/spec.mdимеет секцию## Feature files touched, указывающую наfeatures/<capability>/<name>.feature.- Каждый
.feature-файл в шапке несет# Contributing specs:со списком всех спеков, которые его трогали.
Развязка важна, потому что capability живет дольше любого отдельного спека. Спек 008 «закрытая регистрация» добавил сценарии в features/auth/closed-registration.feature. Спек 014 через год добавит в тот же файл сценарии про rate-limiting. Capability auth - константа. Спеки - timeline.
Для агента это означает: когда задача приходит в контексте «исправь баг в waitlist», агент идет в features/auth/closed-registration.feature, а не пытается восстановить актуальное поведение по chronological order спеков 008-014.
Что писать в .feature: канонический Gherkin + операционные расширения
Cucumber-документация определяет минимальный синтаксис: Feature (заголовок документа), Scenario (один тест-кейс), Given (исходное состояние), When (действие или событие), Then (наблюдаемое последствие), And / But (продолжение цепочки), Background (общий контекст для всех сценариев в файле), Scenario Outline + Examples (один сценарий, прогнанный по таблице данных), теги (@-префикс для метаданных).
Минимальный пример:
Feature: Magic-link sign-in
Background:
Given a clean tenant for this scenario
@smoke @p1
Scenario: User signs in with a fresh magic link
Given a registered user "alice@example.com"
When the user requests a magic link
Then the user receives an email with a one-time link
When the user opens the link within 15 minutes
Then the user is signed in
And a JWT is issued with the user's email claim
Поверх канонического Gherkin полезно зафиксировать несколько операционных правил, которые превращают .feature-файл из теста в контракт.
Капабилити-первая организация, не сервис-первая
Капабилити - то, как продукт-менеджер называет область продукта, а не то, как инженеры разбили код на сервисы. auth/ (капабилити) грубо соответствует services/identity/ (сервис), но это не одно и то же: invariants под features/auth/ описывают user-facing поведение, не внутреннюю архитектуру. Если завтра identity-сервис разрежут на два, features/auth/ не двинется.
Заголовок файла - часть контракта
Полезный паттерн - обязательная шапка файла:
# Capability: auth
# Contributing specs:
# - 008-close-registration (introduces the kill-switch)
# - reference/auth-flows (cross-cutting auth invariants)
# Living docs: docs/bdd/per-file/auth-closed-registration.md
@capability:auth @spec:008 @spec:reference-auth-flows
Feature: Closed registration (waitlist mode)
...
# Capability: и # Contributing specs: - не комментарии, а данные для drift-валидатора. Их формат проверяется парсером.
Whitelist тегов
Каждый сценарий обязан нести минимум: scope-тег (@smoke / @integration / @e2e), priority-тег (@p1 / @p2 / @p3), хотя бы один @spec:<id>, и @user-story:<NNN>-<N>, если контрибутирующий спек имеет user stories. Whitelist:
| Тег | Форма | Назначение |
|---|---|---|
@smoke |
bare | Быстрый PR-чек (default) |
@integration |
bare | Нужны DB / Redis |
@e2e |
bare | Полный pipeline |
@slow |
bare | Исключается из PR fast path |
@flaky, @known-regression |
bare | Требуют # tracking: коммента |
@p1, @p2, @p3 |
bare | Приоритет |
@capability:<slug> |
keyed | Capability-маркер на уровне Feature |
@spec:<id> |
keyed | Контрибутирующий спек, валидируется |
@user-story:<NNN>-<N> |
keyed | Линк на User Story из spec.md |
@owner:<team> |
keyed | Информационный, фильтр в living-doc |
Неизвестный тег ломает pytest-collection. Это ловит опечатки (@intgration) до того, как тест запустится.
Механика «той стороны» Gherkin - как шаги превращаются в исполняемые тесты (декораторы и parsers, target_fixture, World fixture, теги-маркеры, async, doc strings и data tables), трассируемость scenario → step → код и Ruby-эквиваленты (Cucumber, Turnip) - вынесена в отдельный reference: Gherkin и pytest-bdd: как .feature превращается в исполняемые тесты.
Three-direction drift validator как операционный gate
Часть инфраструктуры, который превращает features/ в живой артефакт - валидатор дрейфа. Он смотрит в три направления и срабатывает в трех местах: pre-commit hook, CI, автономный PR-reviewer-агент.
Direction A: spec → features
Когда spec.md меняется (добавили User Story, переписали Acceptance Scenarios), но листинг feature-файлов не меняется - спек описывает поведение, которое контрактом не зафиксировано.
| Правило | Что ловит |
|---|---|
V-SPEC-LIST |
В spec.md нет секции ## Feature files touched |
V-SPEC-LIST-EXISTS |
Путь, перечисленный в этой секции, не существует |
V-USER-STORY |
### User Story N в спеке без матчащего @user-story:<NNN>-<N> тега ни в одном feature-файле |
Direction B: features → spec
Когда .feature-файл указывает на несуществующий спек или вовсе не имеет контрибутирующих спеков.
| Правило | Что ловит |
|---|---|
V-LINK |
# Contributing specs: ссылается на id, которого нет в specs/ |
V-ORPHAN |
.feature без контрибутирующих спеков (warning) |
Direction C: code → features
Когда production-код меняет externally-observable поведение, но ни один feature-файл не тронут.
| Правило | Что ловит |
|---|---|
V-CODE-DRIFT |
PR трогает behaviour-affecting пути под services/*/src/, libs/*/src/, apps/*/src/ без изменения .feature-файлов и без лейбла bdd-skip:no-behaviour-change |
Direction C обычно делегируют автономному PR-reviewer-агенту, который читает diff: программная классификация «behaviour-affecting vs refactor» нетривиальна, а агент с доступом к diff справляется. Reviewer пишет в комментарий PR-а структурированный отчет:
## BDD Spec Coverage
- ✅ No .feature files under specs/ (location rule honoured)
- ✅ specs/017-apple-signin/spec.md lists "Feature files touched"
- ✅ features/auth/apple-signin.feature has @spec:017 in its header
- ✅ Scenario "User signs in with valid Apple ID" tagged @smoke @p1 @spec:017 @user-story:017-1
- ⚠️ Direction C: production code change without features/ update
- Modified: services/orchestration/src/.../doc_matcher.py
- No .feature change in the PR
- Resolve: add a scenario, or apply bdd-skip:no-behaviour-change
- ✅ Coverage gate at 100%
Verdict: REQUEST CHANGES
Кросс-катящие правила
Эти правила срабатывают независимо от направления drift-а - они проверяют целостность самого формата и метаданных, а не связи между деревьями.
V-LOCATION - .feature-файл где-либо под specs/. Это hard-rule из дизайна стека: spec-и - decision history, features - current state, и они не должны смешиваться. Если кто-то по ошибке создал specs/008-close-registration/features/closed-registration.feature, валидатор это ловит и блокирует merge. Без этого правила при росте проекта неизбежно появятся два дерева feature-файлов, между которыми поедет drift.
V-PII - реальный email, телефон или bot-token в шагах сценария. .feature-файлы лежат в публичном (или хотя бы общедоступном для всей компании) репозитории; PII в них - regulatory-проблема (GDPR / 152-ФЗ / CCPA). Реализация - regex-сканирование шагов на паттерны вроде \d{10,15} для телефонов, [A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,} для email-ов кроме whitelisted доменов (@example.com, @test.local), \d{6,}:[A-Za-z0-9_-]{30,} для Telegram bot-token-ов. Cost фалшивого срабатывания - переименовать значение в плейсхолдер; cost пропуска - regulatory finding.
V-TAG - тег не в проектном whitelist. Whitelist живет в одном месте (например, tests/bdd/conftest.py + tools/bdd/validate_drift.py, оба должны быть синхронизированы) и фиксирует допустимые scope, priority, и keyed-теги. Это ловит опечатки (@intgration вместо @integration) и неавторизованные нововведения (один разработчик решил завести @critical - валидатор просит сначала добавить в whitelist).
V-SPEC-TAG - @spec:<id> не резолвится в реальный spec dir. Тег @spec:008 предполагает, что в specs/ есть директория 008-*. Если ее нет (опечатка, спек был удален, или ссылка на еще-не-созданный спек) - валидатор ругается. Это симметрично к V-LINK в направлении B (Direction B ловит ссылки в шапке файла, V-SPEC-TAG - в feature-level и scenario-level тегах).
V-LANG - # language: <X> с X != "en". Если команда выбрала английские Gherkin keywords как стандарт (см. антипаттерны), это правило enforce-ит выбор валидатором. Конкретный X может быть ru, de, fr - V-LANG срабатывает на любой не-en. Команды, выбравшие русский Gherkin, инвертируют правило.
PARSE-ERROR - .feature не парсится Gherkin-парсером. Это catch-all для случаев, когда файл синтаксически сломан (отсутствует Feature:, кривое отступление в Scenario Outline, незакрытая doc string). Срабатывает раньше всех остальных правил - если файл не парсится, проверять его содержимое бессмысленно.
Где это срабатывает
Pre-commit hook лучше делать advisory: инженер видит дрейф до push, но не оказывается заблокирован настолько, чтобы хотеть выключить hooks вообще. CI блокирует - exit code 1 на любой error-level finding. PR-reviewer-агент пишет вердикт в PR-комментарий.
Для агента это означает три независимых места, где он не пропустит обновление контракта: pre-commit покажет ему diff drift до push; CI скажет ему то же самое после push; PR-reviewer сделает финальную проверку с человеком в петле.
End-to-end жизненный цикл с агентом
Сшивание выглядит так на примере новой фичи «sign in with Apple ID» в spec-driven workflow.
Шаг 1: /speckit-specify
/speckit-specify "users can sign in with their Apple ID". Скилл пишет specs/017-apple-signin/spec.md с user stories, scope, requirements. Затем сканирует описание на capability cues (cue «sign in» → auth), предлагает целевой путь features/auth/apple-signin.feature, вызывает scaffolder. Scaffolder создает файл с шапкой, feature-level тегами, Background и одним Scenario: на каждый acceptance summary, отмеченным @spec:017 @user-story:017-N @smoke @p1. После этого скилл дописывает в spec.md секцию ## Feature files touched со ссылкой на новый файл.
Шаг 2: /speckit-plan
Constitution Check запускает validate_drift --spec 017. Проверяет:
## Feature files touchedесть и непустая;- листанный путь существует;
- каждый
### User Story Nвspec.mdимеет матчащий@user-story:017-Nтег в файле; - ни один
.feature-файл не оказался подspecs/.
Если чисто - план READY. Если нет - DRAFT, и инженер (или агент) возвращается в /speckit-specify.
Шаг 3: /speckit-tasks
Генерирует список задач. Для BDD - одна creation-task на новый файл и одна [BDD] wiring-task на каждый acceptance scenario, со статическим AST-сканом существующих tests/bdd/steps/ для подсказок реюза:
- [ ] T010 [US1] [BDD] Wire scenario "User signs in with valid Apple ID"
in features/auth/apple-signin.feature (@spec:017 @user-story:017-1).
Steps to bind:
- Given an Apple ID with email "{email}" → NEW: add to tests/bdd/steps/auth_steps.py
- When the user clicks "Sign in with Apple" → NEW: add to tests/bdd/steps/auth_steps.py
- Then a JWT is issued with the user's email claim → NEW: add to tests/bdd/steps/auth_steps.py
Acceptance: pytest tests/bdd/features/test_auth.py -k "User signs in with valid Apple ID" is green.
Шаг 4: имплементация
Production-код под services/identity/src/.../auth/apple_oauth.py. Step bindings под tests/bdd/steps/auth_steps.py:
from pytest_bdd import given, when, then, parsers
@given(parsers.parse('an Apple ID with email "{email}"'),
target_fixture="apple_id")
def given_apple_id(email: str) -> AppleID:
return AppleID(email=email)
@when('the user clicks "Sign in with Apple"', target_fixture="auth_response")
async def when_apple_signin_clicked(apple_id, identity_app):
return await identity_app.post(
"/auth/apple/init", json={"email": apple_id.email}
)
@then("a JWT is issued with the user's email claim")
def then_jwt_with_email(auth_response, apple_id):
claims = decode_jwt(auth_response.json()["access_token"])
assert claims["email"] == apple_id.email
Эту работу часто делает агент - код прозрачный, паттерны повторяются, fixtures (identity_app, db_session) уже описаны в tests/bdd/conftest.py.
Шаг 5: /speckit-implement
Completion gate запускает:
tools.bdd.render --spec 017для перечисления сценариев спека;pytest tests/bdd/features/test_auth.py -k "<filter>"на этих сценариях;- проверку header back-link (что в
features/auth/apple-signin.featureесть# Contributing specs:с017); tools.bdd.validate_drift --spec 017(drift = 0).
Если все зеленое - спек закрыт. Если нет - gate репортит green / red / pending breakdown и точку отказа.
Шаг 6: PR
CI прогоняет полный pytest + validate_drift. Автономный PR-reviewer-агент работает в isolated context (worktree), читает testing-rules, конституцию, контракты BDD-слоя, выдает «BDD Spec Coverage» секцию с severity matrix. Reviewer не пишет код - только репортит. Решение принимает человек.
Каждый шаг - программный gate с конкретным выходом: либо пайплайн зеленый и контракт в целости, либо красный с конкретной точкой отказа.
Скип-паттерн как backfill-стратегия
Распространенная прагматичная ситуация в момент внедрения BDD-слоя. Чтобы выполнить функциональное требование «every scenario is executable», step bindings нужны для всех сценариев в момент шиппинга. Реальные bindings требуют integration-фикстур (db_session, identity_app, recorded_llm), которые на этом этапе еще не подключены к conftest. Поставлять пустые fixtures ради скорости - значит получать красные тесты, которые никто не починит.
Решение - универсальный single-Given skip-pattern:
@spec:008 @user-story:008-3 @integration @p1
Scenario: New email signup attempt returns 503 with a registration_closed error
Given the new-email-blocked assertions are deferred to Phase 5 auth-domain follow-up
Один Given, который матчится универсальным binding-ом в bdd_meta_steps.py:
@given(parsers.parse("the {workflow} assertions are deferred to {target}"))
def given_deferred(workflow: str, target: str) -> None:
pytest.skip(
f"workflow-level scenario for '{workflow}' is verified manually "
f"per quickstart.md; programmatic binding lands during {target}."
)
При прогоне сценарий рапортуется как skipped с сообщением, в котором есть phase pointer. Pytest-bdd дальше шагов не идет.
Что это удовлетворяет:
- FR-006 «every scenario is executable» (pytest их прогоняет, даже если итог skip);
- V-USER-STORY (каждая User Story имеет тегированный сценарий);
- остальные drift-checks (нет PII, нет неизвестных тегов, валидные spec-линки).
Без принуждения иметь:
- работающий binding для каждого шага сегодня;
- реальные integration-фикстуры для каждой capability сегодня.
Trade-off: скипнутые сценарии не проверяют поведение. Они фиксируют contract surface - что система обещает делать - и показывают, где должны появиться реальные bindings. По мере того, как domain owners пишут реальный код, сценарии переходят из skipped в green.
Что это дает агенту в качестве context
Если перечислить, что меняется в работе агента, когда features/-дерево становится первоклассным контрактом:
- Агент читает
features/<capability>/<name>.featureкак PRD. Не Confluence, не Notion, не Jira-эпик - один файл в репо. Тот же, что читает PM. Это убирает intent gap: задача «исправь баг на онбординге» резолвится вfeatures/auth/...без угадывания по grep-у. - Агент пишет сценарии и тесты в одном файле. Дописать сценарий, попросить pytest-bdd показать missing binding, добавить binding в
tests/bdd/steps/auth_steps.py- это один цикл, не три. - Агент видит границы фичи через теги.
@capability:auth @spec:008- это границы ответственности, машиночитаемые. Агент не правит чужую capability «попутно». - «Готово» имеет программное определение. Сценарий зеленый + drift = 0 + scope-тег есть + spec-тег резолвится + PII-проверка прошла. Агент не закрывает задачу односторонне.
- Living docs отражают актуальное состояние. Рендер из
features/в markdown собирается из последнего main; PM, давая фидбэк по новой версии фичи, читает не спецификацию шестинедельной давности, а текущий контракт.
Это не делает агента «безопасным» автоматически. Discovery остается за людьми, и плохо сформулированные сценарии дадут плохо сделанные фичи. Но операционная дисциплина становится частью инфраструктуры: путь наименьшего сопротивления и правильный путь совпадают.
Антипаттерны и ограничения
- Gherkin не везде уместен. Low-level invariants - индексы БД, internal API contracts, garbage collection - в
.feature-файле выглядят инородно. Их естественное место - обычные интеграционные или unit-тесты. BDD-слой - additive, не replacement: existing pytest-suites остаются на своем месте,features/добавляется сверху как acceptance-контракт. - Happy-path-only сценарии бесполезны. Без edge cases, failure modes, unhappy paths контракт описывает идеальный мир, в котором баги не случаются. Cucumber-документация по example mapping подчеркивает это: зеленые карточки - это конкретные примеры, включая граничные. Полезное правило: для каждого acceptance criteria - хотя бы один happy и один edge сценарий, и явные
@flaky/@known-regressionтеги для зон, где система ведет себя неустойчиво. - Авторинг сценариев без human review. Formulation - часть discovery practice, она требует stakeholder-знания, которого у агента нет. Даже отлично написанный Gherkin не означает правильной модели предметной области. Рабочий pipeline: агент скаффолдит первый драфт, человек проходится перед
/speckit-plan, dispute случаются на этом этапе, не на ревью PR-а. - «У нас есть DDD - этого хватит». Bounded context auth-сервиса содержит регистрацию, login, password reset, MFA, SSO, session management - это разные user-facing capability с разным lifecycle, разными pain points, разными owner-ами. DDD структурирует код, но не functional surface. Подробнее это разобрано в статье «Функциональность как first-class artifact».
- Russian Gherkin keywords. Cucumber поддерживает
# language: ruи весь набор русских ключевых слов (Функция,Сценарий,Допустим,Когда,Тогда). Для проекта с русскоязычной командой это выглядит соблазнительно, но лучше выбирать один стиль и применять его последовательно. Английские keywords сочетаются с большинством IDE-плагинов, GitHub-подсветки и tooling-cookbooks без дополнительных настроек; русский end-user текст и так попадает дословно в step arguments как данные. - Перенос всего PRD в Gherkin. Gherkin - формат для acceptance criteria, не для market analysis, business cases, ARC-design rationale. Spec-документ остается (это decision history),
.feature- его исполняемая часть. Если PM пишет «фича позволит увеличить retention на 15% по гипотезе X» - это не Given/When/Then, это motivation вspec.md. - Drift validator без owner. Правила уровня error без человека, который реагирует на тревоги, превращаются в noise. V-PII или V-LOCATION блокирует merge - кто-то должен быть готов это разрулить. V-ORPHAN - warning, который кто-то периодически разгребает. Без этой ответственности валидатор отключают через две недели.
Минимальная рабочая система
Чтобы попробовать BDD-as-default в проекте с AI-агентами, не обязательно строить весь стек сразу. Минимальная конфигурация.
1. Один capability, не все сразу
Выбрать одну critical capability с понятным actor и outcome - типа onboarding, billing-checkout, primary search. Не размазываться на все области.
2. Top-level features/<capability>/
Не под спеками, не под тестами - отдельное дерево. Один файл на slice (например, features/auth/magic-link.feature).
3. pytest-bdd как execution layer
Установить pytest-bdd, в pyproject.toml положить:
[tool.pytest.ini_options]
asyncio_mode = "auto"
bdd_features_base_dir = "features"
4. Один conftest для tag whitelist
В tests/bdd/conftest.py - pytest_collection_modifyitems, который ловит unknown теги:
_BARE_TAG_WHITELIST = frozenset({
"smoke", "integration", "e2e", "p1", "p2", "p3",
})
_KEYED_TAG_WHITELIST = frozenset({"capability", "spec", "user-story"})
def pytest_collection_modifyitems(config, items):
unknown = []
for item in items:
for marker in list(item.iter_markers()):
name = marker.name
if ":" in name:
if name.split(":", 1)[0] in _KEYED_TAG_WHITELIST:
continue
elif name in _BARE_TAG_WHITELIST:
continue
elif name in {"asyncio", "parametrize", "skip", "skipif", "xfail"}:
continue
unknown.append((item.nodeid, name))
if unknown:
formatted = "\n".join(f" - {nid}: @{tag}" for nid, tag in unknown)
raise pytest.UsageError(f"Unknown tags:\n{formatted}")
5. Per-capability glue в 5 строк
# tests/bdd/features/test_auth.py
from pytest_bdd import scenarios
from tests.bdd.steps.world import world # noqa: F401
from tests.bdd.steps.auth_steps import * # noqa: F403
scenarios("auth")
6. World-фикстура для per-scenario state
# tests/bdd/steps/world.py
from dataclasses import dataclass, field
from typing import Any
import pytest
@dataclass(slots=True)
class World:
last_response: Any = None
extra: dict[str, Any] = field(default_factory=dict)
@pytest.fixture
def world() -> World:
return World()
7. Простейший drift-чек как pre-commit advisory
Скрипт, который:
- проверяет, что
.feature-файлы лежат только подfeatures/; - парсит каждый, смотрит на whitelist тегов;
- pre-commit hook вызывает скрипт, выводит findings, всегда exit 0 (advisory).
Hard gate - в CI: тот же скрипт, но exit 1 при ошибках.
8. Опционально: интеграция со spec-driven workflow
Если используется spec-kit, GitHub Spec Kit, OpenSpec или собственный аналог - добавить вызов scaffold + добавление ## Feature files touched секции в spec-документ. Без этого агент будет писать .feature-файлы вручную, что работает, но не имеет автоматического gate-а.
.pre-commit-config.yaml, кастомный PR-reviewer-агент. Это все имеет смысл, когда capabilities дойдет до 5-10. Через месяц решать: используется ли .feature-дерево как input context для агента (PM ссылается на сценарии, агент в /specify-промтах ставит линки на features/..., тесты падают на каждом дрейфе) - расширять. Если файлы лежат и никто их не открывает - формат не подошел, проблема в process, а не в инфраструктуре.
Источники
Theory and practice
- North, D. «Introducing BDD» (Better Software, March 2006).
- Adzic, G. Specification by Example (Manning, 2011).
- Christensen, C. M. Competing Against Luck (Harper Business, 2016) - JTBD canon.
- Cucumber Project. BDD documentation - cucumber.io/docs/bdd.
- Cucumber Project. Gherkin Reference - cucumber.io/docs/gherkin/reference.
- Cucumber Project. Example Mapping - cucumber.io/docs/bdd/example-mapping.
Industry artifacts
- pytest-bdd - Python BDD framework, integrates with pytest+asyncio+coverage.
- Spec Kit (GitHub) - публичный фреймворк spec-driven development; источник конвенции
specs/NNN-...и/speckit-...скиллов. - AGENTS.md (Linux Foundation, 2025+) - root-level протокол для AI-агентов.
Смежные статьи на сайте
- Функциональность как first-class artifact - feature catalog как соседний слой.
- Context Engineering - контекстное окно как ресурс.
- Инфраструктура контекста для AI-агентов - трёхъярусная архитектура памяти.
- SDLC для AI-агентов - где BDD лежит в spec-driven процессе.
- Spec Kit - конкретный фреймворк spec-driven development.
- Как устроен Claude Code - skills и rules как L4 слой.