Introduction
In 2026, BDD (Behavior-Driven Development) tests with Cucumber remain essential for aligning developers, testers, and product owners on an application's expected behaviors. Cucumber parses feature files in Gherkin language (Given/When/Then) and links them to code via step definitions, turning readable specs into executable automated tests.
Why is it crucial? Picture an agile team: instead of isolated unit tests, Cucumber promotes collaboration through a shared business language, easily integrated into CI/CD pipelines (GitHub Actions, Jenkins). For an intermediate level, we'll go beyond basics: data tables for multiple scenarios, hooks for setup/teardown, and TypeScript config for robustness. This tutorial provides a complete, working project simulating user login with realistic mocks. By the end, you'll bookmark this guide for future sprints. (132 words).
Prerequisites
- Node.js 20+ installed
- JavaScript/TypeScript knowledge
- Editor like VS Code with Cucumber (Gherkin) extension
- Testing basics (assertions, mocks)
- npm or yarn
Initialize the project
mkdir cucumber-bdd-tutorial && cd cucumber-bdd-tutorial
npm init -y
npm install -D @cucumber/cucumber chai @types/chai sinon-chai @types/sinon-chai typescript ts-node @types/node
npx tsc --init --target es2020 --module commonjs --lib es2020 --outDir ./dist --rootDir ./ --strict --esModuleInterop --skipLibCheck
mkdir -p features stepsThese commands create a Node.js project, install Cucumber.js, Chai for assertions, and TypeScript with ts-node for direct execution. The tsconfig is set for ES2020 and CommonJS, compatible with Cucumber. Avoid global versions for CI reproducibility.
Configure TypeScript
Copy the generated tsconfig.json and tweak it if needed. Then create cucumber.js to point to features and steps.
tsconfig.json configuration
{
"compilerOptions": {
"target": "es2020",
"module": "commonjs",
"lib": ["es2020"],
"outDir": "./dist",
"rootDir": "./",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["steps/**/*.ts", "cucumber.js"],
"exclude": ["node_modules", "dist"]
}This tsconfig enables strict mode to catch errors early, targets ES2020 for modern features, and includes TS steps. The outDir separates build from source, avoiding clutter. Test with npx tsc --noEmit to validate.
Cucumber configuration file
const common = [
'features/**/*.feature',
'--require-module ts-node/register',
'--require steps/**/*.ts',
'--format progress-bar',
'--publish-quiet'
];
module.exports = {
default: common.join(' ')
};This JS file configures Cucumber to load TS via ts-node, scan features and steps. The progress-bar format provides quick visual feedback. Add --format json for CI reports later.
Write the Gherkin feature
Create features/connexion.feature with scenarios for successful/failed login and a data table for multiple cases. Gherkin is declarative: it describes what, not how.
Complete feature file
Feature: Connexion utilisateur
En tant qu'utilisateur, je veux me connecter pour accéder au tableau de bord.
Scenario: Connexion réussie
Given l'utilisateur est sur la page de connexion
When il saisit le nom d'utilisateur "admin" et le mot de passe "secret"
Then il est connecté avec succès
Scenario: Connexion échouée
Given l'utilisateur est sur la page de connexion
When il saisit le nom d'utilisateur "foo" et le mot de passe "bar"
Then un message d'erreur s'affiche
Scenario Outline: Connexions multiples avec data table
Given l'utilisateur est sur la page de connexion
When il saisit le nom d'utilisateur <user> et le mot de passe <pass>
Then le résultat est <result>
Examples:
| user | pass | result |
| admin | secret | success |
| foo | bar | failure |This feature covers the happy path, simple failure, and data table for parameterization. Examples make it scalable without duplication. Use Background for shared steps in production.
Step definitions for login
import { Given, When, Then } from '@cucumber/cucumber';
import { expect } from 'chai';
type LoginResult = { user?: string; error?: string };
async function checkLogin(username: string, password: string): Promise<LoginResult> {
// Mock API/DB réaliste
if (username === 'admin' && password === 'secret') {
return { user: 'Admin User' };
}
return { error: 'Identifiants invalides' };
}
let currentUser: string | null = null;
let currentError: string | null = null;
Given('l\'utilisateur est sur la page de connexion', function () {
currentUser = null;
currentError = null;
});
When('il saisit le nom d\'utilisateur {string} et le mot de passe {string}', async function (username: string, password: string) {
const result = await checkLogin(username, password);
currentUser = result.user || null;
currentError = result.error || null;
});
Then('il est connecté avec succès', function () {
expect(currentUser).to.not.be.null;
expect(currentUser).to.equal('Admin User');
});
Then('un message d\'erreur s\'affiche', function () {
expect(currentError).to.not.be.null;
expect(currentError).to.equal('Identifiants invalides');
});
Then('le résultat est {string}', function (expectedResult: string) {
if (expectedResult === 'success') {
expect(currentUser).to.not.be.null;
} else {
expect(currentError).to.not.be.null;
}
});Steps link Gherkin to logic: the checkLogin mock simulates a backend. Global variables manage state (improve with custom World in production). Chai provides fluent assertions; async handles future real APIs.
Add hooks
Hooks handle global setup, logging, screenshots (Cypress-adapted). Keep them separate for modularity.
Hooks for setup and logging
import { Before, After, Status } from '@cucumber/cucumber';
Before({ tags: '@important' }, function () {
console.log('🚀 Début test important');
});
Before(function (scenario) {
console.log(`Test: ${scenario.pickle.name}`);
});
After(function (scenario) {
if (scenario.result!.status === Status.FAILED) {
console.error('❌ Test échoué:', scenario.result!.duration, 'ms');
} else {
console.log('✅ Test réussi');
}
});Before hooks log scenarios, After analyzes status for reports. Tags filter (@important). Use for DB resets or screenshots in e2e, preventing state pollution.
Run the tests
npx cucumber-js
# Ou avec tags
npx cucumber-js --tags "@smoke"
# Format HTML
npx cucumber-js --format html --out report.htmlRuns all tests with default config. --tags filters, --format generates reports. Add to package.json scripts: {test: 'cucumber-js'} for quick npm test.
Best practices
- Avoid duplication: use Scenario Outline + Examples or Background.
- Custom World: extend World interface for shared state without globals (this.user = ...).
- CI/CD integration: Cucumber badges on GitHub, parallel runs with --threads.
- Page Objects: for e2e, encapsulate UI in reusable classes.
- Tags and dry-run:
npx cucumber-js --dry-run --tags @wipto validate without running.
Common errors to avoid
- Poorly escaped regex steps: use {string} for text, {int} for numbers; escape ' with \'.
- Unreset state: always clean up in Given or hooks, or get flaky tests.
- Forgotten async: add async/await on When with APIs.
- Missing types: strict TS catches errors; compile before running.
Next steps
Dive into Cucumber.js docs, integrate with Playwright for e2e, or explore Cucumber JVM for Java. Check our advanced testing courses at Learni to master BDD in teams.