Ваши спецификации Gherkin врут вам.

Не специально. Изначально они были точными. Но через шесть спринтов кто-то отрефакторил checkout flow и забыл обновить шаг When the user submits payment. Файл .feature по-прежнему проходит, потому что step definition всё ещё существует. Он просто вызывает код, который больше не соответствует тому, что scenario на самом деле описывает. У вас зелёные тесты и ложная уверенность. Это стандартная траектория BDD, если вы активно с этим не боретесь.

Проблема не в том, что разработчики ленивы. Дело в том, что связь между файлами .feature и step definitions изначально слабая. Сценарии Gherkin — это строки. Step definitions — это regexes или аннотации, которые сопоставляют эти строки. Ни один компилятор не требует, чтобы изменение сценария влекло за собой соответствующее изменение кода, или наоборот. Тулчейн предполагает, что вы будете вручную поддерживать их синхронизацию. Вы не будете.

Почему ручная дисциплина не работает в масштабе

Каждая команда начинает с одного и того же плана: написать спецификацию, реализовать шаги, обновлять всё вместе. Это работает на первой неделе.

Всё рушится во время рефакторинга. Вы переименовываете domain concept в коде, но Gherkin всё ещё использует старую терминологию, потому что её изменение означает обновление двенадцати feature files и повторное ревью с product. Или вы выделяете новый validation rule, но существующий scenario неявно полагался на старое поведение, и никто этого не заметил, потому что step definition тихо обобщили, чтобы тест продолжал проходить. Спецификации превращаются в параллельную, всё менее точную вселенную.

Цена — это не просто устаревшая документация. Это доверие. Как только разработчики перестают верить, что feature files описывают реальность, они перестают их читать. Затем перестают их писать. И тогда вы возвращаетесь к unit tests с непонятными названиями и без общего языка со stakeholders.

Что на самом деле значит «быть синхронизированным»

Поддерживать спецификации в синхронизации — это не просто заставить тесты проходить. Заставить их проходить легко. Синхронизация означает три вещи:

  1. Каждый шаг Gherkin имеет соответствующий step definition, который делает то, что говорит спецификация.
  2. Каждый step definition реально используется хотя бы в одном scenario.
  3. Язык в спецификации соответствует языку в кодовой базе.

Большинство команд проверяют только первый пункт, и делают это во время выполнения. Вам нужно проверять все три, и делать это в CI до мержа кода.

Автоматическая валидация шагов со строгой привязкой

Свободное строковое сопоставление в инструментах вроде Cucumber — корневая причина. Можно ужесточить его, сделав step definitions first-class references, которые сборка может валидировать.

В проектах на TypeScript или JavaScript можно заменить regex-based step definitions на генерируемый step registry, который сопоставляет шаги Gherkin с реальными function references. Ключевой момент: сопоставление генерируется, а не пишется вручную, поэтому сборка падает, если scenario ссылается на несуществующий шаг.

Вот минимальная настройка с использованием custom parser и generated registry. Сначала парсите файлы .feature at build time:

// scripts/validate-steps.ts
import { readFileSync, readdirSync } from 'fs';
import { parse } from '@cucumber/gherkin';
import { IdGenerator } from '@cucumber/messages';

const featureFiles = readdirSync('./features').filter(f => f.endsWith('.feature'));
const allSteps = new Set<string>();

for (const file of featureFiles) {
  const content = readFileSync(`./features/${file}`, 'utf-8');
  const gherkinDocument = parse(content, new IdGenerator());
  
  for (const feature of gherkinDocument.feature?.children || []) {
    for (const step of feature.scenario?.steps || []) {
      allSteps.add(step.text);
    }
  }
}

// Import the actual step registry from your test code
import { stepRegistry } from '../steps/registry';

const registeredSteps = new Set(Object.keys(stepRegistry));
const undefinedSteps = [...allSteps].filter(s => !registeredSteps.has(s));
const orphanedSteps = [...registeredSteps].filter(s => !allSteps.has(s));

if (undefinedSteps.length > 0) {
  console.error('Undefined steps:', undefinedSteps);
  process.exit(1);
}

if (orphanedSteps.length > 0) {
  console.error('Orphaned steps:', orphanedSteps);
  process.exit(1);
}

console.log(`Validated ${allSteps.size} steps against ${registeredSteps.size} definitions.`);

Ваш step registry предоставляет функции по их точному тексту Gherkin:

// steps/registry.ts
import { given, when, then } from './step-helpers';

export const stepRegistry: Record<string, Function> = {
  'the user is logged in': given.theUserIsLoggedIn,
  'the user adds an item to the cart': when.theUserAddsAnItemToTheCart,
  'the total should be {int}': then.theTotalShouldBe,
};

Объекты given, when и then — это обычные модули с функциями. Никакого regex magic. Если разработчик меняет текст Gherkin, он должен добавить соответствующую запись в registry, иначе сборка упадёт. Если он удаляет scenario, orphaned step detection поймает оставшееся definition.

Интеграция в CI до мержа

Скрипт, который разработчики запускают локально, — это скрипт, который они забывают запускать. Нужно сделать так, чтобы валидация ломала сборку.

Добавьте её в ваш test pipeline:

# .github/workflows/ci.yml
jobs:
  validate-specs:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - run: npm ci
      - run: npx ts-node scripts/validate-steps.ts
      - run: npm test

Важная деталь: validate-steps.ts запускается до актуального test suite. Если есть mismatch между feature files и step definitions, вы хотите fail fast с понятной ошибкой, а не прогонять сотню cucumber scenarios, которые могут тихо проходить на stale logic.

Живая документация требует генерируемых отчётов

Валидация поддерживает синтаксис в порядке, но не гарантирует, что спецификации читаемы или полезны. Для этого нужен living documentation pipeline, который генерирует HTML reports из ваших feature files и публикует их при каждом merge to main.

Инструменты вроде Cucumber Reports или Pickles могут превратить файлы .feature в browsable docs. Ключевой момент: документы генерируются из тех же файлов, которые валидирует CI. Если scenario удалён, он исчезает из документов. Если язык меняется, документы обновляются автоматически. Нет second source of truth, который нужно поддерживать.

Публикуйте отчёт как artifact в CI или деплойте его на static site:

# .github/workflows/docs.yml
jobs:
  publish-docs:
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      - run: npm install -g @picklesdoc/pickles
      - run: pickles --feature-directory=./features --output-directory=./docs
      - uses: actions/upload-pages-artifact@v3
        with:
          path: ./docs

Stakeholders не нужно читать raw Gherkin. Им нужна читаемая страница, в актуальность которой они верят. Автоматизация строит это доверие.

Компромисс: строгость против выразительности

Подход с registry имеет свою цену. Вы теряете гибкость regex patterns вроде /^the user adds (\d+) items? to the cart$/. Каждый вариант становится explicit entry или parameterized step с typed placeholders. Это verbose.

Альтернатива — оставить regexes, но добавить более строгий linter, который предупреждает, когда pattern слишком broad или когда текст шага не совпадает ни с одним known pattern. Можно получить 80% safety при 20% verbosity, используя встроенные флаги Cucumber dry-run и publish в сочетании с custom linter, который проверяет unused step definitions.

# Dry-run parses all features without executing them, surfacing undefined steps
npx cucumber-js --dry-run

Это менее строго, чем подход с registry. Он ловит undefined steps, но не orphaned ones, и не обеспечивает semantic alignment. Для команд с большими existing suites это pragmatic starting point. Для новых проектов подход с registry окупается в течение месяца.

Что мы пробовали, и это не сработало

Мы экспериментировали с генерацией Gherkin из code comments. Идея была в том, что разработчики будут аннотировать свои test methods, а инструмент будет производить файлы .feature. Это провалилось, потому что Gherkin должен быть читаем для non-developers. Generated prose из имён методов нечитаем. Это даже не prose.

Мы также пробовали навязывать pair programming для каждого изменения спецификации. Это помогло, но не масштабировалось. Проблема механическая, и решение тоже должно быть механическим.

Начните с обнаружения undefined steps сегодня

Если у вас уже есть Cucumber suite, самое малое полезное изменение — добавить --dry-run в ваш CI pipeline. Это занимает пять минут, и это поймает самый распространённый drift: отрефакторенный scenario, который больше не совпадает ни с одним step definition.

Если вы начинаете с чистого листа, рассмотрите registry-based approach. Первоначальные затраты на explicit mappings окупаются build-time guarantees и уверенностью свободно рефакторить, не беспокоясь о том, что ваши спецификации тихо устаревают.

Ваши спецификации Gherkin должны описывать, что делает система. Если вы не можете доверять им в этом, они просто expensive comments. Автоматизируйте проверки, которые поддерживают их в честности, или примите, что они будут врать вам.