35BDD with Cucumber
What you will master here
- What BDD is and when it pays off
- Gherkin syntax: Feature, Scenario, Given/When/Then, Background, Examples
- Step definitions in JS/TS
- Hooks (Before, After, BeforeAll, AfterAll), tags, scenario outlines
- Cucumber + Playwright integration
- Anti-patterns: "scripted Gherkin", over-abstracted steps
35.1 What BDD really is
Behaviour-Driven Development: a collaboration technique where business, developers and testers agree on behaviour using a shared, near-English vocabulary before writing code. The artifact (a .feature file) doubles as both the spec and the automated test.
BDD is NOT just "writing tests in Gherkin". If your business doesn't read or contribute to the .feature files, you're getting all the syntax overhead and none of the collaboration benefit — use plain code tests instead.
35.2 Gherkin syntax
Feature: User authentication
As a registered user
I want to sign in to my account
So that I can access my dashboard
Background:
Given the system has a user "alice@test.com" with password "secret"
@smoke @auth
Scenario: Successful sign-in
Given I am on the sign-in page
When I enter "alice@test.com" and "secret"
And I click "Sign in"
Then I should see "Welcome, Alice"
And I should be on the dashboard
Scenario: Wrong password
Given I am on the sign-in page
When I enter "alice@test.com" and "wrong"
And I click "Sign in"
Then I should see an error "Invalid credentials"
And I should remain on the sign-in page
Scenario Outline: Account lockout after <attempts> bad attempts
Given I am on the sign-in page
When I attempt to sign in <attempts> times with wrong password
Then the account should be <state>
Examples:
| attempts | state |
| 3 | active |
| 5 | locked |
| 10 | locked |
Keywords
| Keyword | Purpose |
|---|---|
Feature | Top-level grouping — one capability of the system |
Background | Steps that run before every Scenario in the Feature |
Scenario | One specific example of behaviour |
Given | Context / preconditions |
When | The action under test |
Then | Expected outcome |
And / But | Continuation — same as previous keyword |
Scenario Outline + Examples | Parameterised scenario, one row per case |
| @tag | Filter / categorise (run only @smoke, exclude @wip) |
35.3 Step definitions in TS
npm i -D @cucumber/cucumber @playwright/test
// steps/auth.steps.ts
import { Given, When, Then } from '@cucumber/cucumber';
import { expect } from '@playwright/test';
import { World } from '../world'; // custom World (see below)
Given('the system has a user {string} with password {string}',
async function (this: World, email: string, password: string) {
await this.api.post('/test/users', { data: { email, password } });
});
Given('I am on the sign-in page', async function (this: World) {
await this.page.goto('/login');
});
When('I enter {string} and {string}',
async function (this: World, email: string, password: string) {
await this.page.getByLabel('Email').fill(email);
await this.page.getByLabel('Password').fill(password);
});
When('I click {string}', async function (this: World, label: string) {
await this.page.getByRole('button', { name: label }).click();
});
Then('I should see {string}', async function (this: World, text: string) {
await expect(this.page.getByText(text)).toBeVisible();
});
35.4 The "World" — per-scenario context
// world.ts
import { setWorldConstructor, World as CW } from '@cucumber/cucumber';
import { Browser, BrowserContext, Page, chromium, APIRequestContext, request } from 'playwright';
export class World extends CW {
browser!: Browser; context!: BrowserContext; page!: Page; api!: APIRequestContext;
async open() {
this.browser = await chromium.launch();
this.context = await this.browser.newContext();
this.page = await this.context.newPage();
this.api = await request.newContext({ baseURL: 'https://api.example.com' });
}
async close() {
await this.api.dispose();
await this.context.close();
await this.browser.close();
}
}
setWorldConstructor(World);
35.5 Hooks
// hooks.ts
import { Before, After, BeforeAll, AfterAll, Status } from '@cucumber/cucumber';
import { World } from './world';
BeforeAll(async () => { /* one-time global setup */ });
AfterAll(async () => { /* one-time global teardown */ });
Before(async function (this: World, scenario) { await this.open(); });
After(async function (this: World, scenario) {
if (scenario.result?.status === Status.FAILED) {
const buf = await this.page.screenshot();
this.attach(buf, 'image/png');
}
await this.close();
});
35.6 Tags & selective runs
npx cucumber-js --tags "@smoke and not @wip" npx cucumber-js --tags "@auth or @checkout" npx cucumber-js features/auth.feature
35.7 Reports
// cucumber.json (config)
{
"default": {
"paths": ["features/**/*.feature"],
"require": ["steps/**/*.ts", "hooks.ts", "world.ts"],
"requireModule": ["ts-node/register"],
"format": ["progress-bar", "html:reports/cucumber.html", "json:reports/cucumber.json"]
}
}
The HTML report is shareable with non-tech stakeholders — one of the few real BDD wins.
35.8 Common BDD anti-patterns
- Imperative Gherkin — "When I click the button with id
btn-pri" leaks implementation. Use declarative — "When I submit the form". - Bloated Background — runs before every scenario. Keep it small and necessary.
- Step explosion — every feature defines new
When I do Xsteps that look similar. Refactor common verbs. - Over-abstracted steps —
When I do the thingtells nobody anything. Stay readable. - Gherkin without business — if business never reads it, you're paying syntax tax for no payoff.
- Scenario as full workflow — 20-step scenarios collapse. Keep scenarios atomic; one observation per scenario.
35.9 When NOT to use Cucumber
| Don't use Cucumber when… | Do use it when… |
|---|---|
| Only engineers maintain tests | BAs/PMs write or review scenarios |
| You want refactor-friendly tests | Living documentation is a deliverable |
| Type safety on test code matters | You're explaining to auditors what's tested |
| You want native Playwright UI mode | Stakeholder reports are required |
Module 27 — BDD/Cucumber Q&A bank
What is BDD?
Behaviour-Driven Development — a collaboration practice where business, dev and QA agree on behaviour using shared near-English language (Gherkin) before coding. The same artifact becomes the automated test. BDD is about the process; Cucumber is one tool that supports it.
What does Gherkin look like?
Feature header, optional Background, one or more Scenarios. Each scenario uses Given (context) / When (action) / Then (outcome), optionally chained with And/But. Scenario Outline + Examples for parameterised cases. Tags for filtering.
What's the difference between Background and Hooks?
Background is Gherkin — visible in the .feature, runs before each scenario in that feature. Hooks (Before/After) are code — invisible to readers, run around scenarios or features. Use Background for spec-visible setup; hooks for plumbing (browser open/close).
What's the World in Cucumber?
A per-scenario context object. Each scenario gets a fresh instance, so state doesn't bleed between scenarios. You define what it holds (browser, page, API client) and access via
this in step definitions.How do you parameterise scenarios?
Scenario Outline with an Examples table. Use
<name> placeholders in the steps; the outline runs once per row. Lets you cover multiple data sets without duplicating prose.How do you filter which scenarios run?
Tags. Mark scenarios with
@smoke / @wip / @auth. Run with --tags "@smoke and not @wip". Same expression syntax for "or", "not", parens.When does Cucumber NOT pay off?
When only engineers read/write/maintain the .feature files. You're paying syntax overhead (step definitions, regex matching, world setup) for no collaboration benefit. Plain Playwright tests are faster to write and refactor.
What's a "good" vs "bad" scenario?
Good: declarative, business-oriented, one observation. "When the customer submits the cart, the order is created with the cart total." Bad: imperative, UI-coupled. "When I click
#submit with timeout 5s, then I see element .success." The latter leaks implementation and breaks when UI changes.How would you integrate Cucumber with Playwright?
Install
@cucumber/cucumber + playwright. In a World class hold a Browser/Context/Page/APIRequestContext, lifecycle via Before/After hooks. Step definitions interact with this.page. Run via cucumber-js. You lose Playwright's built-in test runner features (projects, trace UI), gain stakeholder-readable specs.How do you handle async in Cucumber steps?
Step definitions are async functions; return the Promise.
Given('...', async function () { await this.page.goto('/x'); }). Cucumber waits for the returned Promise to settle.How would you avoid step explosion?
Use Cucumber expressions or regex patterns to share steps across features.
When I click {string} with a parameter covers all button-click variants instead of one step per button. Refactor repeated verbs into shared definitions.What's the value of the HTML report?
It shows each scenario in Gherkin form with pass/fail and embedded screenshots. Non-engineers can read it and understand what was tested and what failed — a real win when audit / compliance / business stakeholders want to see test outcomes.