Половина ваших сценариев Cucumber пропущена. Не упала. Пропущена.
Этот жёлтый статус хуже, чем красная сборка. Он заставляет набор выглядеть здоровым, скрывая под одной вежливой меткой три совершенно разные проблемы. Пропущенный сценарий может означать, что его отфильтровал tag filter, пропал step definition, или Before-хук выбросил исключение ещё до того, как выполнился первый шаг. Ваша задача — выяснить, какой паразит обосновался в вашем наборе.
Почему “Skipped” — самый опасный статус в Cucumber
У Cucumber три терминальных состояния для сценария: passed, failed и skipped. Passed и failed честны. Skipped — это мусорное ведро.
Когда сценарий пропущен, Cucumber сообщает, что некоторое предусловие не было выполнено. Проблема в том, что это “предусловие” может быть чем угодно: от намеренного tag filter до null pointer в setup-хуке. Набор с 50% пропущенных сценариев выглядит так, будто прогнаны 500 тестов. На самом деле прогнаны 250 тестов, а остальные 250 просто обойдены без проверки.
Большинство CI-дашбордов считают skipped нейтральным. Ваш пайплайн зелёный. Но половина спецификаций поведения не проверяется. Если вы используете Cucumber как живую документацию, половина вашей документации — ложь.
Три корневые причины массового пропуска
Существует ровно три способа получить пропущенный сценарий в Cucumber. Два из них — баги. Один — misconfiguration, которая выглядит как фича.
Tag filters, которые исключают больше, чем вы планировали
Самая распространённая причина 50% пропущенных сценариев — tag expression в конфиге раннера, которая шире, чем вы думаете. Ваш CI-профиль может выглядеть так:
// cucumber.js
module.exports = {
default: [
'--format', 'progress',
'--tags', 'not @wip',
].join(' '),
ci: [
'--format', 'json:reports/cucumber.json',
'--tags', 'not @wip and not @slow',
].join(' '),
};
Настоящий урон наносят positive tag filters. Фильтр вроде --tags '@regression' запускает только сценарии с тегом @regression. Все остальные пропускаются. Именно так вы переходите от 100% выполненных тестов к 50%, не меняя ни одного feature file.
Step definitions, которые больше не матчатся
Cucumber пропускает весь сценарий, если хотя бы один шаг не имеет matching step definition. Это не failure. Это skip. Ваш CI остаётся зелёным. Это случается чаще, чем вы ожидаете. Продакт-менеджер редактирует Gherkin-файл, чтобы исправить опечатку в шаге:
# features/checkout.feature
Feature: Checkout
Scenario: Guest user completes purchase
Given a guest user with items in cart
When they proceed to checkout
Then the order total should include tax
Ваши step definitions ожидают вот это:
// features/step_definitions/checkout_steps.js
Given('a guest user with items in the cart', function () {
// setup
});
Обратите внимание на пропущенный артикль “the” в feature file. Regex Cucumber больше не матчится. Весь сценарий пропускается. Умножьте это на команду, которая рефакторит feature files, не прогоняя полный набор локально, и вы можете потерять половину покрытия из-за дрейфа.
Падающие хуки, которые маскируются под skips
Если Before-хук выбрасывает исключение, Cucumber пропускает каждый сценарий в этом feature file. Он не помечает их как failed. Он пропускает их.
// features/support/hooks.js
const { Before } = require('@cucumber/cucumber');
Before(async function () {
this.browser = await chromium.launch();
this.page = await this.browser.newPage();
// Если здесь вылетит ошибка, каждый сценарий в файле будет пропущен
await this.page.goto(process.env.TEST_URL);
});
Если TEST_URL не определена в CI, хук падает, и каждый привязанный к нему сценарий пропускается. Ваша сборка зелёная. Ваши тесты бесполезны.
Как диагностировать, какой паразит у вас завёлся
Вы не можете починить 50% пропущенных сценариев, не зная, какая из трёх причин виновата. Стандартный вывод Cucumber не скажет вам. Вам придётся его допросить.
Запустите набор с --dry-run и без tag filters:
npx cucumber-js --dry-run --tags ''
Dry run парсит каждый feature file и матчит каждый шаг со step definition, ничего не выполняя. Если показывает 100% undefined — у вас дрейф step definitions. Если показывает 100% passed — ваши skips идут от tag filters или runtime-хуков.
JSON formatter включает поле status для каждого шага. Сценарий, пропущенный из-за tag filter, не будет иметь вообще никаких шагов. Сценарий, пропущенный из-за отсутствующего step definition, покажет undefined на несматчившемся шаге. Сценарий, пропущенный из-за падающего хука, покажет skipped на каждом шаге, а массив embeddings может содержать ошибку хука.
npx cucumber-js --format json:report.json
Спарсите JSON и посчитайте, сколько сценариев имеет ноль шагов, а сколько — шаги со статусом undefined или skipped. Это скажет вам, куда смотреть.
Самое безопасное долгосрочное решение — считать любой неожиданный skip за failure сборки. Небольшой post-processor для JSON-отчёта справляется с этим:
// scripts/fail-on-skips.js
const fs = require('fs');
const report = JSON.parse(fs.readFileSync('report.json', 'utf8'));
let unexpectedSkips = 0;
for (const feature of report) {
for (const element of feature.elements) {
if (element.type !== 'scenario') continue;
const hasSkip = element.steps.some(s => s.result?.status === 'skipped');
const hasUndefined = element.steps.some(s => s.result?.status === 'undefined');
// Разрешаем @wip пропускаться; всё остальное должно выполняться
const isWip = element.tags?.some(t => t.name === '@wip');
if ((hasSkip || hasUndefined) && !isWip) {
console.error(`Unexpected skip: ${feature.name} > ${element.name}`);
unexpectedSkips++;
}
}
}
if (unexpectedSkips > 0) {
console.error(`\n${unexpectedSkips} scenario(s) skipped unexpectedly. Failing build.`);
process.exit(1);
}
Запускайте это после каждого вызова Cucumber в CI. Это поймает misconfiguration тегов, отсутствующие step definitions и падающие хуки до того, как они тихо сгниют ваш набор.
Компромисс: почему некоторый skipping допустим
Намеренное пропускание — это валидный инструмент. Тег @wip существует не просто так. Вы пишете сценарий до имплементации, помечаете его @wip, и раннер пропускает его, пока код не готов.
Разница между здоровым skipping и гнилью набора — в гигиене. @wip должен быть временным. Он должен жить в ветке, а не на main шесть месяцев. Если 50% ваших сценариев помечены @wip на дефолтной ветке, у вас нет тестового набора. У вас список желаний.
Tag-based filtering также имеет смысл для тестов, специфичных для окружения. Сценарий, требующий физический платёжный терминал, не должен запускаться в CI. Но такой сценарий должен быть помечен @hardware, а не @slow или @manual. Будьте явны в том, почему что-то исключено, и аудируйте эти исключения на code review так же, как вы аудируете сам код.
Как остановить гниль
Если сегодня у вас 50% пропущенных, вот самый быстрый путь обратно к честности.
-
Запустите dry run без тегов. Посчитайте
undefined-шаги. Почините каждое несоответствие. Обычно это пятиминутная работа find-and-replace. -
Проаудитируйте ваши runner profiles. Перечислите каждое tag expression в вашем
cucumber.js, Mavenpom.xmlили Gradle-конфиге. Подтвердите, что каждое намеренно. Замените positive filters (--tags '@regression') на negative (--tags 'not @wip'), чтобы новые сценарии по умолчанию выполнялись. -
Добавьте skip-check скрипт в CI. Пусть он ломает сборку при любом неожиданном skip. Это разовая настройка, которая окупается навсегда.
-
Планируйте ежемесячный tag audit. Ищите в feature files
@wipи считайте их. Если число растёт, у вас проблема с процессом, а не с тулингом.
FAQ
Почему Cucumber пропускает сценарии вместо того, чтобы падать, когда отсутствует step definition?
Cucumber считает отсутствующий step definition неполной спецификацией, а не дефектом кода. Сценарий пропускается, потому что Cucumber не может выполнить то, чего не понимает. Это историческое поведение из оригинальной Ruby-имплементации, и оно сохраняется во всех портах. Единственный способ заставить его падать — добавить post-processor, который проверяет статусы undefined.
В чём разница между skipped scenario и pending scenario?
В современных версиях Cucumber “pending” — это step-level статус, который явно выбрасывается из step definition (например, вызов pending() в Ruby или возврат 'pending' в JavaScript). “Skipped” — это scenario-level статус, который применяется, когда падает предусловие, tag filter исключает сценарий, или предыдущий шаг упал. На практике оба отображаются жёлтым и оба означают “это не было выполнено до конца”.
Можно ли заставить Cucumber падать при отсутствующих step definitions? В большинстве портов Cucumber нет встроенного CLI-флага для этого. Вам придётся парсить JSON или JUnit output и ломать сборку самостоятельно. Скрипт в этом посте — минимальный рабочий пример.
Как найти, какие теги реально используются в моём наборе?
Запустите grep по feature files: grep -roh '@[a-zA-Z0-9_-]*' features/ | sort | uniq -c | sort -rn. Это даст вам частотную таблицу каждого тега. Сопоставьте её с вашими runner profiles, чтобы найти теги, которые фильтруют без документации.