BDD with Cucumber

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

KeywordPurpose
FeatureTop-level grouping — one capability of the system
BackgroundSteps that run before every Scenario in the Feature
ScenarioOne specific example of behaviour
GivenContext / preconditions
WhenThe action under test
ThenExpected outcome
And / ButContinuation — same as previous keyword
Scenario Outline + ExamplesParameterised scenario, one row per case
@tagFilter / 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

35.9 When NOT to use Cucumber

Don't use Cucumber when…Do use it when…
Only engineers maintain testsBAs/PMs write or review scenarios
You want refactor-friendly testsLiving documentation is a deliverable
Type safety on test code mattersYou're explaining to auditors what's tested
You want native Playwright UI modeStakeholder 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.