Half your Cucumber scenarios are skipped. Not failed. Skipped.

That yellow status is worse than a red build. It makes your suite look healthy while hiding three completely different problems under one polite label. A skipped scenario could mean your tag filter excluded it, a step definition went missing, or a Before hook threw an exception before the first step even ran. Your job is to figure out which parasite is living in your suite.

Why “Skipped” Is Cucumber’s Most Dangerous Status

Cucumber has three terminal states for a scenario: passed, failed, and skipped. Passed and failed are honest. Skipped is a garbage can.

When a scenario is skipped, Cucumber is telling you that some prerequisite was not met. The problem is that “prerequisite” can be anything from a deliberate tag filter to a null pointer in a setup hook. A suite with 50% skipped scenarios looks like it ran 500 tests. It didn’t. It ran 250 tests and waved at the other 250 without checking them.

Most CI dashboards treat skipped as neutral. Your pipeline is green. But half your behavior specifications are not being verified. If you are using Cucumber as living documentation, half your documentation is a lie.

The Three Root Causes of Mass Skipping

There are exactly three ways to generate a skipped scenario in Cucumber. Two of them are bugs. One is a misconfiguration that looks like a feature.

Tag filters that exclude more than you intended

The most common cause of 50% skipped scenarios is a tag expression in your runner config that is broader than you think. Your CI profile might look like this:

// cucumber.js
module.exports = {
  default: [
    '--format', 'progress',
    '--tags', 'not @wip',
  ].join(' '),

  ci: [
    '--format', 'json:reports/cucumber.json',
    '--tags', 'not @wip and not @slow',
  ].join(' '),
};

The real damage happens when teams use positive tag filters. A filter like --tags '@regression' runs only scenarios tagged @regression. Every other scenario is skipped. This is how you go from 100% executed to 50% executed without changing a single feature file.

Step definitions that no longer match

Cucumber skips an entire scenario if any step lacks a matching step definition. This is not a failure. It is a skip. Your CI stays green. This happens more often than you’d expect. A product manager edits a Gherkin file to fix a typo in a step:

# 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

Your step definitions expect this:

// features/step_definitions/checkout_steps.js
Given('a guest user with items in the cart', function () {
  // setup
});

Notice the missing “the” in the feature file. Cucumber’s regex no longer matches. The entire scenario skips. Multiply this across a team that refactors feature files without running the full suite locally, and you can lose half your coverage to drift.

Failing hooks that mask themselves as skips

If a Before hook throws an exception, Cucumber skips every scenario in that feature file. It does not fail them. It skips them.

// features/support/hooks.js
const { Before } = require('@cucumber/cucumber');

Before(async function () {
  this.browser = await chromium.launch();
  this.page = await this.browser.newPage();
  // If this throws, every scenario in the file is skipped
  await this.page.goto(process.env.TEST_URL);
});

If TEST_URL is undefined in CI, the hook throws and every scenario attached to it is skipped. Your build is green. Your tests are worthless.

How to Diagnose Which Parasite You Have

You cannot fix 50% skipped scenarios without knowing which of the three causes is responsible. Cucumber’s default output does not tell you. You have to interrogate it.

Run your suite with --dry-run and no tag filters:

npx cucumber-js --dry-run --tags ''

A dry run parses every feature file and matches every step to a step definition without executing anything. If it shows 100% undefined, you have step definition drift. If it shows 100% passed, your skips are coming from tag filters or runtime hooks.

The JSON formatter includes a status field for each step. A scenario skipped by a tag filter will have no steps at all. A scenario skipped by a missing step definition will show undefined on the mismatched step. A scenario skipped by a failing hook will show skipped on every step, and the embeddings array might contain the hook error.

npx cucumber-js --format json:report.json

Parse the JSON and count how many scenarios have zero steps versus how many have steps with undefined or skipped status. That tells you where to look.

The safest long-term fix is to treat any unexpected skip as a build failure. A small post-processor for the JSON report does the job:

// 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');

    // Allow @wip to skip; everything else must run
    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);
}

Run this after every Cucumber invocation in CI. It will catch tag misconfigurations, missing step definitions, and failing hooks before they silently rot your suite.

The Trade-Off: Why Some Skipping Is Okay

Deliberate skipping is a valid tool. The @wip tag exists for a reason. You write the scenario before the implementation, mark it @wip, and let the runner skip it until the code is ready.

The difference between healthy skipping and suite rot is hygiene. @wip should be temporary. It should live on a branch, not on main for six months. If 50% of your scenarios are tagged @wip on your default branch, you do not have a test suite. You have a wish list.

Tag-based filtering also makes sense for environment-specific tests. A scenario that requires a physical payment terminal should not run in CI. But that scenario should be tagged @hardware, not @slow or @manual. Be explicit about why something is excluded, and audit those exclusions in code review the same way you audit the code itself.

How to Stop the Rot

If you are at 50% skipped today, here is the fastest path back to honesty.

  1. Run a dry run with no tags. Count the undefined steps. Fix every mismatch. This is usually a five-minute find-and-replace job.

  2. Audit your runner profiles. List every tag expression in your cucumber.js, Maven pom.xml, or Gradle config. Confirm that each one is intentional. Replace positive filters (--tags '@regression') with negative ones (--tags 'not @wip') so new scenarios run by default.

  3. Add the skip-check script to CI. Make it fail the build on any unexpected skip. This is a one-time setup that pays off forever.

  4. Schedule a monthly tag audit. Search your feature files for @wip and count them. If the number is growing, you have a process problem, not a tooling problem.


FAQ

Why does Cucumber skip scenarios instead of failing them when a step definition is missing? Cucumber treats a missing step definition as an incomplete specification, not a code defect. The scenario is skipped because Cucumber cannot execute what it does not understand. This is historical behavior from the original Ruby implementation, and it persists across ports. The only way to make it fail is to add a post-processor that checks for undefined statuses.

What’s the difference between a skipped scenario and a pending scenario? In modern Cucumber versions, “pending” is a step-level status thrown explicitly by a step definition (for example, calling pending() in Ruby or returning 'pending' in JavaScript). “Skipped” is a scenario-level status applied when a prerequisite fails, a tag filter excludes the scenario, or a prior step failed. In practice, both show up as yellow and both mean “this did not run to completion.”

Can I make Cucumber fail on missing step definitions? There is no built-in CLI flag for this in most Cucumber ports. You have to parse the JSON or JUnit output and fail the build yourself. The script in this post is a minimal working example.

How do I find which tags are actually being used in my suite? Run a grep across your feature files: grep -roh '@[a-zA-Z0-9_-]*' features/ | sort | uniq -c | sort -rn. This gives you a frequency table of every tag. Cross-reference that against your runner profiles to find tags that filter without documentation.