On this page9 sections
CommandsIntermediate7-9 min reference

Cucumber & Gherkin

A practical reference for Cucumber's plain-language test format and the step-definition glue that makes it executable. Examples cover both the JavaScript/TypeScript runner (@cucumber/cucumber) and Cucumber-JVM.

Gherkin Syntax

Gherkin is the human-readable DSL. A .feature file uses these keywords.

KeywordPurpose
Feature:Top-level — name and (optional) narrative for the file
Background:Steps that run before every scenario in the file
Rule:Group scenarios under a business rule (Gherkin 6+)
Scenario: / Example:A single test case
Scenario Outline: / Scenario Template:Parameterised scenario; pair with Examples:
Given / When / ThenStep keywords — context, action, outcome
And / ButContinuation of the previous keyword (reads naturally)
*Generic step bullet — same as the previous keyword
#Line comment
Feature: User Login
  As a registered user
  I want to log in to my account
  So that I can access my dashboard
 
  Background:
    Given the login page is displayed
 
  Scenario: Successful login with valid credentials
    When I enter username "testuser@qa.codes"
    And I enter password "SecurePass123"
    And I click the login button
    Then I should be redirected to the dashboard
    And I should see a welcome message
 
  Scenario: Invalid password
    When I enter username "testuser@qa.codes"
    And I enter password "wrong"
    And I click the login button
    Then I should see the error "Invalid email or password"

Scenario Outline & Examples

Use Scenario Outline when the same flow runs with different data. Placeholders use <name> syntax.

Scenario Outline: Login with different roles
  Given I am a user with role "<role>"
  When I log in with "<username>" and "<password>"
  Then I should see the "<dashboard>" dashboard
 
  Examples: Valid users
    | role   | username       | password  | dashboard |
    | admin  | admin@test.com | Admin123  | Admin     |
    | editor | edit@test.com  | Edit123   | Editor    |
    | viewer | view@test.com  | View123   | Viewer    |
 
  Examples: Inactive users
    | role     | username        | password  | dashboard |
    | inactive | dorm@test.com   | Dorm123   | Locked    |

Cucumber generates one scenario per row. Each Examples block can be tagged separately (e.g. @smoke on the first table, @regression on the second).

Data Tables

Pass tabular data directly into a step. The step text always ends with : when followed by a table.

Given the following users exist:
  | name  | email             | role   |
  | Ada   | ada@test.com      | admin  |
  | Bob   | bob@test.com      | viewer |
  | Carol | carol@test.com    | editor |
 
When I delete the users:
  | ada@test.com   |
  | bob@test.com   |
 
Then the user store should contain:
  | email           |
  | carol@test.com  |

Reading data tables — TypeScript / JavaScript

import { Given, DataTable } from "@cucumber/cucumber";
 
Given("the following users exist:", async function (table: DataTable) {
  const users = table.hashes();
  // [{ name: "Ada", email: "ada@test.com", role: "admin" }, ...]
 
  const raw = table.raw();
  // [["name", "email", "role"], ["Ada", "ada@test.com", "admin"], ...]
 
  const rows = table.rows();
  // [["Ada", "ada@test.com", "admin"], ...]   — header dropped
 
  for (const u of users) {
    await db.users.insert(u);
  }
});
 
// Single column → list
Given("the user emails:", function (table: DataTable) {
  const emails = table.raw().flat();    // ["ada@test.com", "bob@test.com"]
});
 
// Two columns → key-value map
Given("the config:", function (table: DataTable) {
  const config = table.rowsHash();      // { base_url: "https://...", timeout: "30" }
});

Reading data tables — Java

@Given("the following users exist:")
public void theFollowingUsersExist(DataTable table) {
  List<Map<String, String>> users = table.asMaps();
  List<List<String>> raw           = table.asLists();
  List<String> emails              = table.asList();          // single column
  Map<String, String> config       = table.transpose().asMap(); // 2 columns → map
}

Step Definitions

TypeScript / JavaScript

import { Given, When, Then } from "@cucumber/cucumber";
import { expect } from "chai";
 
Given("I am on the {string} page", async function (pageName: string) {
  await this.page.goto(`/${pageName.toLowerCase()}`);
});
 
When("I click the {string} button", async function (label: string) {
  await this.page.getByRole("button", { name: label }).click();
});
 
Then("I should see {int} results", async function (count: number) {
  const items = await this.page.getByTestId("result").all();
  expect(items).to.have.lengthOf(count);
});
 
Then("I should see an error", async function () {
  await expect(this.page.getByRole("alert")).toBeVisible();
});

Java

@Given("I am on the {string} page")
public void iAmOnThePage(String pageName) {
  driver.get(BASE_URL + "/" + pageName.toLowerCase());
}
 
@When("I click the {string} button")
public void iClickTheButton(String label) {
  driver.findElement(By.xpath(
    "//button[normalize-space(.)='" + label + "']")).click();
}
 
@Then("I should see {int} results")
public void iShouldSeeResults(int count) {
  List<WebElement> rows = driver.findElements(By.cssSelector("[data-testid=result]"));
  Assert.assertEquals(count, rows.size());
}

Cucumber Expressions vs regex

Cucumber Expressions (the {string}, {int} form) are simpler and the recommended default. Regex is the escape hatch.

ExpressionMatchesCaptured as
{string}"quoted" or 'quoted' textString
{int}integersint / number
{float}floatsfloat / number
{word}a single non-whitespace wordString
{} (anonymous)any contenttype inferred from step definition

Custom parameter type — TypeScript:

import { defineParameterType } from "@cucumber/cucumber";
 
defineParameterType({
  name: "color",
  regexp: /red|green|blue|yellow/,
  transformer: (s) => s,
});
 
When("I select the {color} card", function (color) { /* ... */ });

Hooks

Hooks live in step-definition files (or any file Cucumber loads).

TypeScript / JavaScript

import { Before, After, BeforeAll, AfterAll, BeforeStep, AfterStep, Status } from "@cucumber/cucumber";
 
BeforeAll(async function () {
  // runs once before any scenario
  await migrate();
});
 
Before({ tags: "@smoke" }, async function () {
  // runs only for scenarios tagged @smoke
  await seedSmokeData();
});
 
Before(async function ({ pickle }) {
  this.startedAt = Date.now();
  console.log(`▶ ${pickle.name}`);
});
 
After(async function ({ result, pickle }) {
  if (result?.status === Status.FAILED) {
    const png = await this.page.screenshot();
    this.attach(png, "image/png");
  }
  await this.page.context().close();
});
 
BeforeStep(function ({ pickleStep }) { /* ... */ });
AfterStep(function ({ pickleStep, result }) { /* ... */ });
 
AfterAll(async function () {
  await teardown();
});

Java

import io.cucumber.java.Before;
import io.cucumber.java.After;
import io.cucumber.java.Scenario;
 
public class Hooks {
  @Before
  public void setUp() {
    driver = new ChromeDriver();
  }
 
  @Before("@smoke")
  public void smokeOnly() {
    seedSmokeData();
  }
 
  @After(order = 0)
  public void tearDown(Scenario scenario) {
    if (scenario.isFailed()) {
      byte[] png = ((TakesScreenshot) driver).getScreenshotAs(OutputType.BYTES);
      scenario.attach(png, "image/png", "failure");
    }
    driver.quit();
  }
}

Hook ordering

  • BeforeAllBefore (high order first) → step → After (low order first) → AfterAll
  • Multiple Before hooks: declared order in JS, order attribute in Java (lower = earlier).
  • Multiple After hooks: reverse of Before — last declared runs first.

Tags & Filtering

Tag features and scenarios with @name. Tags on a Feature apply to every scenario in the file.

@smoke
Feature: Login
 
  @critical
  Scenario: Valid login
    ...
 
  @wip
  Scenario: Reset password
    ...

Running specific tags

# Cucumber-JS
npx cucumber-js --tags "@smoke"
npx cucumber-js --tags "@smoke and not @wip"
npx cucumber-js --tags "(@login or @signup) and @critical"
 
# Cucumber-JVM (with Maven)
mvn test -Dcucumber.filter.tags="@smoke and not @wip"

Common tag conventions

TagMeaning
@smokeCritical-path subset — runs first, fails fast
@regressionFull suite — runs nightly
@wipWork in progress — exclude from default runs
@skip / @ignorePermanently disabled (with reason in the file)
@manualDocuments flows that aren't automated yet
@flakyQuarantine — re-runs allowed

Doc Strings

For step arguments that span multiple lines or contain special characters.

Given the following JSON payload:
  """json
  {
    "name": "Test User",
    "email": "test@qa.codes",
    "roles": ["admin", "editor"]
  }
  """
 
When I POST it to "/api/users"
Then the response should be:
  """xml
  <user>
    <id>42</id>
    <email>test@qa.codes</email>
  </user>
  """

Receiving in step definitions:

Given("the following JSON payload:", function (payload: string) {
  this.body = JSON.parse(payload);
});
@Given("the following JSON payload:")
public void theFollowingJsonPayload(String payload) {
  this.body = new ObjectMapper().readTree(payload);
}

The optional json / xml after the opening """ is a content-type hint — it doesn't change parsing.

World Object & Context Sharing

Steps run in the same scenario need to share state — the user that was created in Given, the response captured in When, etc.

TypeScript — World

import { setWorldConstructor, World, IWorldOptions } from "@cucumber/cucumber";
import { Browser, Page, chromium } from "playwright";
 
export class TestWorld extends World {
  browser!: Browser;
  page!: Page;
  user?: { id: number; email: string };
  apiResponse?: Response;
 
  constructor(options: IWorldOptions) {
    super(options);
  }
 
  async launchBrowser() {
    this.browser = await chromium.launch();
    this.page = await this.browser.newPage();
  }
}
 
setWorldConstructor(TestWorld);
 
// In a step:
Given("a user is logged in", async function (this: TestWorld) {
  this.user = await api.createUser();
  await this.page.goto("/login");
  // ...
});

Java — DI containers

Cucumber-JVM doesn't have a World; you use a DI container instead.

// PicoContainer is bundled — easiest option
public class TestContext {
  public WebDriver driver;
  public User currentUser;
  public Response apiResponse;
}
 
public class LoginSteps {
  private final TestContext ctx;
 
  public LoginSteps(TestContext ctx) {     // PicoContainer wires this
    this.ctx = ctx;
  }
 
  @Given("a user is logged in")
  public void aUserIsLoggedIn() {
    ctx.currentUser = api.createUser();
    // ...
  }
}

For Spring, swap to cucumber-spring and annotate with @SpringBootTest. For Guice, use cucumber-guice.

Reporting

Built-in formatters

npx cucumber-js --format progress           # dots
npx cucumber-js --format pretty             # human-readable
npx cucumber-js --format json:report.json   # machine output
npx cucumber-js --format html:report.html   # built-in HTML
npx cucumber-js --format @cucumber/pretty-formatter

Multiple formatters can run at once — declare each with --format.

Cucumber HTML report

The official HTML formatter (@cucumber/html-formatter) ships an interactive report with feature/scenario hierarchy, attachments (screenshots, logs), and step timing.

npx cucumber-js --format html:reports/cucumber.html

Allure adapter

# Install allure-cucumberjs (or allure-cucumber for JVM)
npm install -D allure-cucumberjs allure-commandline
 
# In cucumber config, add the formatter:
# format: ['allure-cucumberjs/reporter']
 
# After the run:
npx allure generate ./allure-results --clean -o allure-report
npx allure open allure-report

JUnit-XML for CI

npx cucumber-js --format junit:reports/results.xml

GitHub Actions / Jenkins / GitLab pick this format up natively. Pair with a JUnit reporter action so failures surface in PR checks.

MasterThought (Cucumber-JVM)

The de-facto HTML reporter for the JVM ecosystem — feeds off Cucumber JSON output:

mvn test -Dcucumber.plugin="json:target/cucumber.json"
# Then a Maven plugin or a Jenkins step generates HTML from the JSON.