Software Testing  

TDD/BDD with Cucumber in JavaScript

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

  1. Write a failing feature (undefined steps) that expresses business behavior.

  2. Generate step definitions (pending), then implement minimal automation to pass.

  3. Refactor page objects/steps; remove duplication; keep steps declarative.

  4. 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. [1] @badeball/cypress-cucumber-preprocessor (npm): https://www.npmjs.com/package/@badeball/cypress-cucumber-preprocessor

  2. [2] @badeball/cypress-cucumber-preprocessor (GitHub): https://github.com/badeball/cypress-cucumber-preprocessor

  3. [3] Cypress reporters docs: https://docs.cypress.io/app/tooling/reporters

  4. [4] Cucumber reporting: https://cucumber.io/docs/cucumber/reporting/

  5. [5] Cucumber.js parallel execution (GitHub docs): https://github.com/cucumber/cucumber-js/blob/main/docs/parallel.md

  6. [6] Playwright fixtures docs: https://playwright.dev/docs/test-fixtures

  7. [7] multiple-cucumber-html-reporter (npm): https://www.npmjs.com/package/multiple-cucumber-html-reporter.