1. Architecture Overview
Cucumber parses Gherkin features → maps steps to step-definition functions → uses an automation engine (Cypress or Playwright) to drive the application under test. Scenarios are isolated; fixtures/hooks prepare and dispose of the browser state per scenario.
2. TDD/BDD Workflow
Write a failing feature (undefined steps) that expresses business behavior.
Generate step definitions (pending), then implement minimal automation to pass.
Refactor page objects/steps; remove duplication; keep steps declarative.
Add negative and edge cases; iterate.
3. Gherkin Guidelines
Keep steps business-level (intent) and avoid UI details in Gherkin.
Use Scenario Outlines for data-driven variations; Background for essentials only.
Tag scenarios to control suites: @smoke, @regression, @wip, @api, etc.
Limit scenarios to 3–5 steps; prefer one when per scenario; deterministic assertions.
4. Example Feature (Common)
features/login.feature
Feature: Login
@smoke
Scenario: Successful login
Given a registered user exists
When they sign in with valid credentials
Then they are redirected to the dashboard
5. Stack A — Cypress + Cucumber
5.1 Install
npm i -D cypress @badeball/cypress-cucumber-preprocessor @bahmutov/cypress-esbuild-preprocessor
5.2 Project Layout
/cypress
/e2e # .feature files (Cypress v10+)
/steps # step definitions (ts/js)
/pages # page objects/helpers
/support # custom commands, before/after
/reports
cypress.config.ts
5.3 Cypress Configuration
// cypress.config.ts
import { defineConfig } from "cypress";
import createBundler from "@bahmutov/cypress-esbuild-preprocessor";
import addCucumberPreprocessorPlugin from "@badeball/cypress-cucumber-preprocessor";
import { createEsbuildPlugin } from "@badeball/cypress-cucumber-preprocessor/esbuild";
export default defineConfig({
e2e: {
specPattern: "**/*.feature",
setupNodeEvents: async (on, config) => {
await addCucumberPreprocessorPlugin(on, config);
on(
"file:preprocessor",
createBundler({ plugins: [createEsbuildPlugin(config)] })
);
return config;
},
baseUrl: "https://app.example.com",
},
reporter: "junit",
reporterOptions: {
mochaFile: "reports/junit/results-[hash].xml",
toConsole: true,
},
});
5.4 Step Definitions (Cypress)
// cypress/steps/login.steps.ts
import { Given, When, Then } from "@badeball/cypress-cucumber-preprocessor";
Given("a registered user exists", () => {
// Seed via API if available
// cy.request("POST", "/test/seed", { user: "alice" })
});
When("they sign in with valid credentials", () => {
cy.visit("/login");
cy.get("#username").type("alice");
cy.get("#password").type("Password!23", { log: false });
cy.get("#login").click();
});
Then("they are redirected to the dashboard", () => {
cy.location("pathname").should("eq", "/dashboard");
cy.contains("h1", "Dashboard").should("be.visible");
});
5.5 Cucumber HTML Reporting (post-run)
# install
npm i -D multiple-cucumber-html-reporter
// scripts/report.js
const report = require("multiple-cucumber-html-reporter");
report.generate({
jsonDir: "./reports/cucumber-json",
reportPath: "./reports/html",
metadata: { browser: { name: "chrome" }, device: "CI", platform: { name: "linux" } },
});
5.6 Notes
Prefer API/DB fixtures for Given; keep UI for behavior under test.
Use Cypress retries intelligently; avoid arbitrary waits.
Parallelization: shard specs via CI or consider Cypress Cloud; Cucumber JSON can be merged for unified HTML reports.
6. Stack B — Playwright + Cucumber.js
6.1 Install
npm i -D @cucumber/cucumber playwright typescript ts-node
npx playwright install
6.2 Project Layout
/features # .feature files
/src/steps # step defs
/src/hooks # Before/After, screenshots
/src/pages # page objects
/reports # json/html reports
cucumber.js # cucumber config
6.3 Cucumber.js Configuration
// cucumber.js
module.exports = {
default: {
require: ["src/steps/**/*.ts", "src/hooks/**/*.ts"]
publishQuiet: true,
format: ["progress", "json:reports/cucumber.json"],
parallel: 4,
worldParameters: { baseUrl: "https://app.example.com" }
}
};
6.4 Hooks (per-scenario browser isolation)
// src/hooks/hooks.ts
import { Before, After, setDefaultTimeout } from "@cucumber/cucumber";
import { chromium, Browser, BrowserContext, Page } from "playwright";
setDefaultTimeout(60_000);
declare global {
var __pw__: { browser?: Browser, context?: BrowserContext, page?: Page };
}
Before(async function ()
global.__pw__ = {};
global.__pw__.browser = await chromium.launch({ headless: true });
global.__pw__.context = await global.__pw__.browser.newContext();
global.__pw__.page = await global.__pw__.context.newPage();
});
After(async function (scenario) {
if (scenario.result?.status === "failed" && global.__pw__?.page) {
await global.__pw__.page.screenshot({ path: `reports/${Date.now()}-failed.png` });
}
await global.__pw__?.context?.close();
await global.__pw__?.browser?.close();
});
6.5 Step Definitions (Playwright)
// src/steps/login.steps.ts
import { Given, When, Then } from "@cucumber/cucumber";
import { expect } from "expect";
Given("a registered user exists", async function () {
// Seed via API/DB if available
});
When("they sign in with valid credentials", async function () {
const page = global.__pw__!.page!;
await page.goto("https://app.example.com/login");
await page.fill("#username", "alice");
await page.fill("#password", "Password!23");
await page.click("#login");
});
Then("they are redirected to the dashboard", async function () {
const page = global.__pw__!.page!;
await page.waitForURL("**/dashboard");
const h1 = page.locator("h1");
await expect(await h1.textContent()).toContain("Dashboard");
});
6.6 Reporting & Parallel
Enable parallel workers in cucumber.js (e.g., parallel: 4).
Emit JSON (json:reports/cucumber.json) and generate HTML via multiple-cucumber-html-reporter.
7. CI Integration (GitHub Actions Example)
name: e2e
on: [push, pull_request]
jobs:
bdd:
runs-on: ubuntu-latest
strategy:
matrix:
engine: [cypress, playwright]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm run test:${{ matrix.engine }}
- run: npm run report
- uses: actions/upload-artifact@v4
with:
name: reports-${{ matrix.engine }}
path: reports/**
8. Best Practices & Guardrails
One browser context per scenario for isolation: no shared mutable state.
Use API/DB for setup/teardown where possible: minimize UI-only setup.
Keep steps reusable and declarative by centralizing selectors in pages.
Fail fast: assert key state early; avoid fixed waits: prefer explicit waits.
Tag management: map @smoke to PR builds, @regression to nightly; exclude @wip from CI.
9. Troubleshooting
Undefined steps: Ensure step text matches exactly; re-generate snippets.
Flaky waits: Replace timeouts with deterministic waits (element states, network, URL).
Parallel issues: Ensure test data isolation, avoid global singletons, and maintain unique users/records per scenario.
Reports empty: Confirm JSON output path is correct and unique per shard before HTML generation.
References
[1] @badeball/cypress-cucumber-preprocessor (npm): https://www.npmjs.com/package/@badeball/cypress-cucumber-preprocessor
[2] @badeball/cypress-cucumber-preprocessor (GitHub): https://github.com/badeball/cypress-cucumber-preprocessor
[3] Cypress reporters docs: https://docs.cypress.io/app/tooling/reporters
[4] Cucumber reporting: https://cucumber.io/docs/cucumber/reporting/
[5] Cucumber.js parallel execution (GitHub docs): https://github.com/cucumber/cucumber-js/blob/main/docs/parallel.md
[6] Playwright fixtures docs: https://playwright.dev/docs/test-fixtures
[7] multiple-cucumber-html-reporter (npm): https://www.npmjs.com/package/multiple-cucumber-html-reporter.