Cucumber & BDD Testing — Complete Guide
In this tutorial, you'll learn about Cucumber & BDD Testing. We cover key concepts, practical examples, and best practices.
Cucumber BDD testing enables teams to write acceptance tests in plain language using Gherkin, bridging gaps between developers, testers, and business stakeholders.
What You'll Learn
You'll learn how to write Gherkin feature files, implement step definitions in JavaScript and TypeScript, configure Cucumber, integrate with Selenium and Playwright for browser automation, and add BDD tests to your CI/CD pipeline.
Why BDD Matters
Traditional testing separates the people who understand the problem (business stakeholders) from the people who write the tests (developers). Requirements get lost in translation. BDD solves this by creating a single source of truth — feature files that everyone can read. At DodaTech, every feature in Durga Antivirus Pro starts as a Gherkin scenario, ensuring security requirements are never lost in translation.
Real-World Use
A fintech team uses BDD to verify loan approval workflows. The business analyst writes Gherkin scenarios describing eligibility rules. Developers automate step definitions that call the loan decision API. Every release, 200+ scenarios run in CI — catching regressions before they reach customers.
BDD Learning Path
flowchart LR A[Testing Basics] --> B[Test-Driven Development] B --> C[Cucumber & BDD
You are here] C --> D[Cucumber + Selenium] C --> E[Cucumber in CI/CD] style C fill:#f90,color:#fff
What Is BDD?
Behavior-Driven Development (BDD) is a software development methodology that extends TDD by describing application behavior in plain language that everyone — developers, testers, product managers — can understand.
Think of BDD like writing a recipe. A recipe says "Heat the oven to 350°F. Mix flour and sugar. Bake for 30 minutes." Anyone can follow it, regardless of cooking skill. Gherkin is the language of these "recipe" tests.
The Three Amigos
BDD works best when three roles collaborate on each scenario:
| Role | Contribution |
|---|---|
| Business Analyst | Defines what the feature should do |
| Developer | Implements the automation behind each step |
| Tester | Identifies edge cases and validation rules |
Gherkin Syntax
Gherkin is a plain-text language that uses keywords to describe software behavior. Every feature file uses the .feature extension.
Feature: User Login
Feature: User Login
As a registered user
I want to log into the application
So that I can access my dashboard
Background:
Given the database contains a user with email "alice@example.com" and password "ValidP@ss1"
Scenario: Successful login with valid credentials
Given I am on the login page
When I enter email "alice@example.com"
And I enter password "ValidP@ss1"
And I click the login button
Then I should see the dashboard
And I should see "Welcome, Alice"
Scenario: Login with invalid password
Given I am on the login page
When I enter email "alice@example.com"
And I enter password "wrongpass"
And I click the login button
Then I should see an error message "Invalid email or password"
The keywords Feature, Background, Scenario, Given, When, Then, and And are the building blocks. Given sets up state, When performs an action, and Then verifies the result.
Scenario Outlines
Use Scenario Outlines to test the same flow with different data:
Scenario Outline: Password strength validation
Given I am on the registration page
When I enter password "<password>"
Then I should see strength level "<level>"
Examples:
| password | level |
| abc | Weak |
| Password1 | Medium |
| P@ssw0rd!Secure | Strong |
This runs the same scenario three times, once for each row in the Examples table. Scenario Outlines eliminate duplicate feature files and make data-driven testing easy to read.
Step Definitions
Step definitions map each Gherkin line to executable code. Cucumber matches steps by their text pattern — the step definition text must match the Gherkin line exactly.
const { Given, When, Then } = require('@cucumber/cucumber');
const { expect } = require('@playwright/test');
const { LoginPage } = require('./page-objects/LoginPage');
Given('I am on the login page', async function () {
this.loginPage = new LoginPage(this.page);
await this.loginPage.goto();
});
When('I enter email {string}', async function (email) {
await this.loginPage.enterEmail(email);
});
When('I enter password {string}', async function (password) {
await this.loginPage.enterPassword(password);
});
When('I click the login button', async function () {
await this.loginPage.clickLogin();
});
Then('I should see the dashboard', async function () {
await expect(this.page.locator('.dashboard')).toBeVisible();
});
Then('I should see {string}', async function (message) {
await expect(this.page.locator('body')).toContainText(message);
});
Then('I should see an error message {string}', async function (message) {
await expect(this.page.locator('.error')).toContainText(message);
});
The {string} parameter captures quoted values from the Gherkin step and passes them as arguments. Cucumber also supports {int}, {float}, and {word} for different parameter types.
TypeScript Version
import { Given, When, Then } from '@cucumber/cucumber';
import { expect, Page } from '@playwright/test';
import { LoginPage } from './page-objects/LoginPage';
Given('I am on the login page', async function (this: { page: Page }) {
const loginPage = new LoginPage(this.page);
await loginPage.goto();
});
When('I enter email {string}', async function (this: { page: Page }, email: string) {
const loginPage = new LoginPage(this.page);
await loginPage.enterEmail(email);
});
Then('I should see the dashboard', async function (this: { page: Page }) {
await expect(this.page.locator('.dashboard')).toBeVisible();
});
The type annotations help JavaScript developers catch errors at compile time. Cucumber's TypeScript support works out of the box — just install @cucumber/cucumber and configure ts-node in your project.
Cucumber Configuration
Cucumber needs a configuration file to know where feature files and step definitions live:
// cucumber.js — Cucumber configuration
module.exports = {
default: {
require: [
'step-definitions/**/*.js', // Step definition files
'support/**/*.js', // Support files (hooks, world)
],
format: [
'json:reports/cucumber-report.json',
'html:reports/cucumber-report.html',
'progress',
],
paths: ['features/**/*.feature'],
publishQuiet: true,
retry: 1,
},
};
The require array tells Cucumber where to find step definitions and support files. The format array defines reporters — JSON for CI tools, HTML for team review, and progress for console output.
World and Hooks
The World object is the shared context across all step definitions in a single scenario. Hooks run setup and teardown code before and after each scenario.
// support/world.js
const { setWorldConstructor } = require('@cucumber/cucumber');
const { chromium } = require('playwright');
class CustomWorld {
constructor({ parameters }) {
this.context = null;
this.page = null;
}
async init() {
const browser = await chromium.launch({ headless: true });
this.context = await browser.newContext();
this.page = await this.context.newPage();
}
}
setWorldConstructor(CustomWorld);
// support/hooks.js
const { Before, After } = require('@cucumber/cucumber');
Before(async function () {
await this.init();
});
After(async function () {
await this.page?.close();
await this.context?.close();
});
Every scenario gets a fresh browser context. This guarantees test isolation — no shared state between tests.
BDD Workflow
flowchart TD
subgraph "BDD Workflow"
A[Business writes
Feature File
in Gherkin] --> B[Dev writes
Step Definitions]
B --> C[Cucumber
Runner executes
scenarios]
C --> D{All steps
pass?}
D -->|Yes| E[Green: Feature
implemented correctly]
D -->|No| F[Red: Fix step
definitions or
application code]
F --> B
end
style E fill:#4caf50,color:#fff
style F fill:#f44336,color:#fff
The BDD cycle is collaborative. Business stakeholders write the "what" (feature files). Developers write the "how" (step definitions). Cucumber connects them and reports whether the application behaves as expected.
Integration with Selenium and Playwright
Cucumber works with any browser automation tool. Step definitions call the automation API internally:
- Selenium: Use
driver.findElement()andWebDriverWaitinside step definitions. Best for teams already invested in Selenium infrastructure. - Playwright: Use
page.locator()with auto-waiting. Step definitions are cleaner because Playwright waits for elements automatically before interacting with them.
BDD in CI/CD
Add Cucumber tests to your CI/CD pipeline to run scenarios on every commit:
# .github/workflows/bdd.yml
name: BDD Tests
on: [push, pull_request]
jobs:
cucumber:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx playwright install --with-deps chromium
- run: npm run test:bdd
- uses: actions/upload-artifact@v4
if: always()
with:
name: cucumber-report
path: reports/cucumber-report.html
Expected output:
15 scenarios (15 passed)
90 steps (90 passed)
0m12.345s (executing steps: 0m10.123s)
Every PR runs 90+ BDD steps. If any scenario fails, the pipeline fails — no broken code reaches production.
Security Angle: Testing Authentication Flows
BDD is particularly powerful for security testing. You can write Gherkin scenarios that verify authentication, authorization, and session management — critical paths that must work correctly in every release.
Feature: Session Security
Scenario: Session expires after inactivity
Given I am logged in as a valid user
When I wait for 30 minutes without any action
Then I should be redirected to the login page
And my session token should be invalidated
Scenario: SQL injection attempt is blocked
Given I am on the login page
When I enter email "' OR 1=1 --"
And I enter password "anything"
And I click the login button
Then I should see an error message
And I should remain on the login page
At DodaTech, these security scenarios run before every Durga Antivirus Pro release, preventing authentication bypass vulnerabilities from reaching users.
Common Cucumber & BDD Errors
1. Step Definition Not Found
UNDEFINED: Step "I enter email alice@example.com"
Gherkin steps and step definition patterns must match exactly, including quoted parameters.
Fix: Run Cucumber with --format usage to see undefined steps with their suggested pattern code.
2. Overly Specific Step Definitions
Given('I enter email "alice@example.com"', ...)
Hardcoding values makes step definitions unusable across scenarios.
Fix: Use {string} parameters: Given('I enter email {string}', ...).
3. Shared World State Between Scenarios
Scenarios that share browser state will fail when run in parallel or in a different order.
Fix: Reset state in a Before hook — create a fresh browser context for every scenario.
4. Flaky Tests from Missing Waits
Browser automation without proper waits fails intermittently when pages load slowly.
Fix: Use Playwright's auto-waiting or add explicit waitForSelector in Selenium step definitions.
5. Feature Files That Are Too Long
A single feature file with 50 scenarios is hard to maintain and slow to execute.
Fix: Split features by user capability. One feature file per logical feature. Keep each file under 15 scenarios.
6. Focusing Only on the Happy Path
Testing only successful login misses validation errors, network failures, and permission issues.
Fix: For every happy-path scenario, write at least one negative scenario (invalid input, expired session, unauthorized access).
7. Ignoring Data Setup and Cleanup
Tests that assume specific database state fail when run against different environments.
Fix: Use Background steps or API calls to set up test data, and clean up in After hooks.
Practice Questions
1. What is the difference between a Scenario and a Scenario Outline?
A Scenario tests a single specific case. A Scenario Outline runs the same steps multiple times with different data from an Examples table.
2. What do the keywords Given, When, and Then represent?
Given sets up preconditions (state), When performs an action to trigger behavior, and Then verifies the result. And and But extend any of these keywords.
3. Why should step definitions avoid hardcoded values?
Hardcoded values make steps specific to one test case. Parameters ({string}, {int}) make steps reusable across scenarios with different inputs.
4. How does BDD differ from TDD?
TDD writes unit tests before implementation (developer-focused). BDD writes behavioral scenarios in plain language (team-focused) that can drive both development and acceptance testing across the whole team.
5. What is the role of the World object in Cucumber?
The World object is a shared context across all step definitions in a scenario. It stores browser instances, session data, and any state needed between steps.
6. Challenge: Refactor brittle BDD tests
You join a team with 500+ Cucumber scenarios that are slow and flaky. Identify the top 3 issues likely causing the problems and propose a refactoring plan. Include how you'd prioritize fixes and what metrics would show improvement.
Mini Project: BDD Calculator
Write a Cucumber test suite for a calculator application:
Feature: Calculator
Scenario: Add two numbers
Given I have a calculator
When I enter 5 and 3
And I press add
Then the result should be 8
const { Given, When, Then } = require('@cucumber/cucumber');
const { expect } = require('chai');
Given('I have a calculator', function () {
this.calculator = new Calculator();
});
When('I enter {int} and {int}', function (a, b) {
this.a = a;
this.b = b;
});
When('I press add', function () {
this.result = this.calculator.add(this.a, this.b);
});
Then('the result should be {int}', function (expected) {
expect(this.result).to.equal(expected);
});
Expected output:
1 scenario (1 passed)
4 steps (4 passed)
0m0.042s
Try this yourself: Extend the calculator with subtract, multiply, and divide operations. Write Scenario Outlines to test each operation with multiple inputs.
FAQ
What's Next
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro