Key Takeaways
- Cucumber connects readable Gherkin scenarios to executable code called step definitions.
- BDD is a collaborative development practice, not a test-file format. Cucumber supports it but doesn't create shared understanding by itself.
- Cucumber Expressions provide readable parameters such as {string} and {int}; regular expressions remain supported.
- Scenario Outlines turn each Examples data row into a separate scenario, using the same matching step definitions.
Cucumber testing uses Gherkin scenarios as executable specifications: readable examples of expected behavior connected to automated checks. Its value is giving developers, test engineers, and business stakeholders something concrete to discuss and verify—not simply translating test code into English.
What Is Cucumber?
Cucumber runs scenarios written in Gherkin by matching their steps to programming-language functions. Those functions can exercise application code, call an API, or drive a browser; Cucumber doesn't require every scenario to be an end-to-end UI test.
The official implementation directory lists Cucumber-JS for JavaScript, Cucumber-JVM for Java and Kotlin, and Cucumber-Ruby for Ruby, among others. Python tools such as behave and pytest-bdd belong to the wider Gherkin-oriented BDD ecosystem; they aren't official Cucumber implementations.
What Is BDD, and How Does It Relate to TDD?
Behavior-driven development (BDD) uses concrete examples to discover requirements, agree on expected behavior, and guide implementation. It was influenced by test-driven development (TDD), but it isn't merely TDD with plain-language test cases.
TDD uses a failing test, implementation, and refactoring cycle to guide development. BDD also emphasizes deciding which behavior matters and developing a shared vocabulary across roles. You can use both: agree on an acceptance example together, then use lower-level tests while implementing it. Those checks still provide the familiar benefits of unit testing.
Cucumber's BDD guidance describes three connected practices:
- Discovery: discuss rules, examples, and unanswered questions before coding.
- Formulation: express agreed examples clearly enough to review and automate.
- Automation: connect those examples to the system and check its behavior.
Authorship varies by team. A developer or test engineer might write the Gherkin after a conversation with a product owner. Stakeholders don't all need to edit feature files, but the examples should reflect their shared understanding.
How Cucumber Works
A feature file describes behavior. Step definitions connect that description to executable checks. The runner loads the definitions, parses the scenarios, matches each step, executes the matching code, and reports the results.
.feature file
|
v
Parse scenarios and expand Scenario Outlines
|
v
Match step text to loaded step definitions
|
v
Execute application actions and assertions
|
v
Report passed, failed, undefined, or ambiguous resultsGherkin Feature Files and the Login Example
According to the Gherkin reference, a .feature file contains one Feature. It can include descriptions, scenarios, Scenario Outlines, rules, and a Background for shared setup. Given describes the starting context, When describes an action, and Then describes an expected outcome; And and But extend the steps.
This schematic template shows the basic structure. Replace its angle-bracketed descriptions yourself; they aren't executable Scenario Outline parameters.
Feature: <description>
As a <role>
I want to <action>
So that <benefit>
Scenario: <scenario description>
Given <starting context>
When <action>
Then <expected outcome>The As a / I want / So that lines are optional user-story description text, not required Gherkin syntax. A concrete login example is easier to discuss:
Feature: User Authentication
As a website user
I want to log into my account
So that I can access my user-specific features
Scenario: Successful login
Given I am on the login page
When I enter valid credentials
Then I should see the dashboardThe scenario states an observable result without specifying field selectors or button clicks. For a real authentication feature, discuss additional rules such as disabled accounts and rejected credentials rather than assuming this happy path defines the whole feature.
Run Cucumber Tests With JavaScript
This example targets Cucumber-JS 13.3.0 on Node.js 24.x, which is supported by that release's runtime requirements. For runtime selection beyond this example, see which version of Node.js to use.
Create the Project and Configuration
Create the following files in an empty directory. Put the complete login feature shown above in features/login.feature.
package.json
cucumber.mjs
src/login-app.js
features/login.feature
features/support/world.js
features/step_definitions/login.jsUse this package.json:
{
"name": "cucumber-login-example",
"private": true,
"type": "module",
"scripts": {
"test": "cucumber-js"
},
"devDependencies": {
"@cucumber/cucumber": "13.3.0"
}
}Configure feature discovery, support-code loading, and reporting in cucumber.mjs:
export default {
paths: ['features/**/*.feature'],
import: [
'features/support/**/*.js',
'features/step_definitions/**/*.js'
],
format: [
'progress',
['html', 'cucumber-report.html'],
['junit', 'cucumber-report.xml']
]
};The Cucumber-JS configuration reference documents paths for feature files and import for support code. These explicit patterns make discovery predictable: both the World setup and the step definitions are loaded.
Add the Application Fixture
Save this as src/login-app.js. An existing account with valid credentials reaches the dashboard; the other combinations reach an error page.
export class LoginApp {
constructor(userStatus = 'existing') {
this.userStatus = userStatus;
this.page = null;
}
openLogin() {
this.page = 'login';
}
logIn(credentials) {
if (this.page !== 'login') {
throw new Error('Open the login page first');
}
this.page =
this.userStatus === 'existing' && credentials === 'valid'
? 'dashboard'
: 'error';
}
}Configure World and Scenario Setup
In Cucumber-JS, the World is an isolated context created for each scenario. Steps and scenario hooks can access it through this, so one step can store information that another step checks.
Save this as features/support/world.js:
import { Before, World, setWorldConstructor } from '@cucumber/cucumber';
import { LoginApp } from '../../src/login-app.js';
class LoginWorld extends World {
constructor(options) {
super(options);
this.app = null;
}
}
setWorldConstructor(LoginWorld);
Before(function () {
this.app = new LoginApp();
});The Before hook gives every scenario a fresh fixture. Use function expressions when accessing this; arrow functions don't receive Cucumber's World as their this value.
For real integrations, use hooks to manage infrastructure: create browser or client resources in Before and release them in After. Make cleanup tolerate partially completed setup. This in-memory example doesn't allocate resources that need teardown.
Implement the Step Definitions
Save this complete module as features/step_definitions/login.js. It supports both the simple scenario and the Scenario Outline introduced next.
import assert from 'node:assert/strict';
import { Given, When, Then } from '@cucumber/cucumber';
Given('I am on the login page', function () {
this.app.openLogin();
});
When('I enter valid credentials', function () {
this.app.logIn('valid');
});
Then('I should see the dashboard', function () {
assert.equal(this.app.page, 'dashboard');
});
Given('my account status is {string}', function (userStatus) {
assert.ok(['existing', 'new'].includes(userStatus));
this.app.userStatus = userStatus;
this.app.openLogin();
});
When('I try to log in with {string}', function (credentials) {
assert.ok(['valid', 'invalid', 'non-existent'].includes(credentials));
this.app.logIn(credentials);
});
Then('I should {string}', function (result) {
const expectedPages = new Map([
['see the dashboard', 'dashboard'],
['see an error message', 'error']
]);
assert.ok(expectedPages.has(result), `Unknown result: ${result}`);
assert.equal(this.app.page, expectedPages.get(result));
});From the project directory, install the dependency and run the feature:
npm install
npm testThe test script runs cucumber-js. The configured formatters write progress to the terminal, an HTML report to cucumber-report.html, and JUnit XML to cucumber-report.xml.
Cucumber Expressions vs. Regular Expressions
A step definition uses an expression to match the text after a Gherkin keyword. Start with Cucumber Expressions: {string} matches quoted text and passes its contents without the quotes, {int} matches an integer, and {word} matches text without whitespace.
| Matching style | How parameters work | When to use it |
|---|---|---|
| Cucumber Expressions | Named parameter types such as {string} and {int} | Readable domain steps and straightforward values |
| Regular expressions | Regular-expression capture groups supply arguments | Text matching that needs more precise pattern control |
You can register custom parameter types for domain values. Don't mix Cucumber Expression placeholders into a regular expression: the two styles use different syntax.
Using Scenario Outlines for Data-Driven Scenarios
A Scenario Outline is a template, not a scenario that runs directly. Each data row beneath an Examples header substitutes values into the matching angle-bracketed placeholders, producing a separate scenario before step matching.
Append this fragment inside the existing Feature in features/login.feature. Here, new means there is no registered account.
# Append inside Feature: User Authentication
Scenario Outline: Login attempt
Given my account status is "<userStatus>"
When I try to log in with "<credentials>"
Then I should "<result>"
Examples:
| userStatus | credentials | result |
| existing | valid | see the dashboard |
| new | non-existent | see an error message |
| existing | invalid | see an error message |For the first row, the Given step becomes my account status is "existing". The expression my account status is {string} then passes existing to the userStatus argument. The credentials and result columns follow the same substitution-and-matching process.
The column names don't automatically bind to JavaScript argument names. Angle brackets perform Gherkin substitution; braces declare expression parameters. The quotes in this outline matter because its definitions use {string}.
Run npm test again. The suite now contains four scenarios: the original successful login plus three examples from the outline. Keep each table focused on one rule rather than accumulating unrelated cases.
Step Discovery, Matching, and Failures
Loading a support-code module registers its definitions; its filename doesn't bind it to a same-named feature. Loaded definitions are available across the suite. The Cucumber execution reference explains the important matching and result rules:
- One match: Cucumber executes the matching definition.
- No match: the step is undefined, and subsequent steps in that scenario are skipped.
- Multiple matches: Cucumber reports ambiguity rather than choosing the first definition.
- Thrown error or rejected promise: the step fails, and subsequent steps in that scenario are skipped.
Given, When, and Then aren't part of matching, so changing the keyword won't disambiguate identical step text. Returning false does not fail a JavaScript step; throw an error or fail an assertion instead. For asynchronous application calls, await the operation and let failures reach Cucumber.
Running Cucumber in CI
Commit the package-lock.json generated by npm install. On a clean CI checkout, npm ci installs from the lockfile and rejects a mismatch with package.json:
npm ci
npm testInstall development dependencies in the test job, since this example declares Cucumber there. In a normal run, failed, undefined, or ambiguous scenarios make the Cucumber-JS command exit unsuccessfully; don't hide that exit status.
The formatter documentation covers HTML and JUnit output. Configure your CI system to ingest cucumber-report.xml and retain cucumber-report.html even when tests fail; creating the files alone doesn't configure CI reporting.
When replacing the fixture with a real application, start the application and wait for readiness before testing. Browser-based suites also need the browser runtime installed. Isolate test data before enabling parallel execution, and keep credentials out of feature files and report attachments.
Common Mistakes
- Treating readable tests as collaboration. Review examples together before implementation, not only after the behavior has shipped.
- Documenting every click. Keep selectors and navigation mechanics in automation code unless those details are the behavior under discussion.
- Being too abstract. Replace statements such as "everything works" with concrete, observable outcomes.
- Putting every test in Gherkin. Reserve scenarios for behavior worth discussing across roles; use direct tests for implementation details.
- Sharing mutable state. Store scenario state on World rather than in module globals, and give concurrent scenarios independent application data.
When Cucumber Is Worth the Extra Layer
Use Cucumber when discussing examples helps your team clarify important behavior and those examples deserve to stay executable. If nobody outside the automation team reads or discusses the scenarios, consider whether ordinary tests would communicate the intent with less maintenance. Readability is useful; shared understanding is the goal.
FAQ
What is Cucumber testing?
Cucumber testing connects Gherkin scenarios to executable step definitions that exercise a system and check expected outcomes. The scenarios can serve as shared, readable specifications when the team discusses and maintains them together.
What is the difference between Cucumber and BDD?
BDD is a collaborative development practice built around discovering, describing, and implementing behavior through examples. Cucumber is one tool for automating those examples; using it doesn't automatically mean a team is practicing BDD.
What programming languages does Cucumber support?
Official implementations include Cucumber-JS for JavaScript, Cucumber-JVM for Java and Kotlin, and Cucumber-Ruby for Ruby. Python tools such as behave and pytest-bdd are separate Gherkin-oriented BDD alternatives, not official Cucumber implementations.
Is Gherkin syntax the same across programming languages?
The core Gherkin language is shared across Cucumber implementations, and its keywords can be localized into different spoken languages. Step-definition APIs and runner configuration are implementation-specific.
How do I run Cucumber tests in CI?
For the JavaScript project shown here, run npm ci followed by npm test, with development dependencies installed. Preserve the command's exit status, configure CI to ingest the JUnit XML report, and retain the HTML report for debugging.
Further Reading
- Cucumber's BDD guide — discovery, formulation, and automation.
- Implementation directory — official implementations and related tools.
- Gherkin reference — feature structure, keywords, and Scenario Outlines.
- Cucumber Expressions — built-in parameters and custom parameter types.
- Cucumber-JS configuration — feature discovery and support-code loading.
- Cucumber-JS World — scenario context and custom World classes.
- Cucumber-JS hooks — setup and teardown responsibilities.
- Cucumber-JS formatters — terminal output and report formats.
