Skip to main content

Cucumber & BDD โ€” The Complete Guide

A single-read, end-to-end reference for Behavior-Driven Development and Cucumber: enough to write a feature file that survives a "three amigos" review, wire up step definitions, or walk into an SDET interview. Organized as a lookup you can also read top-to-bottom.

๐Ÿ“‹ Quick reference: Cucumber & BDD โ†’ Beginner

Prerequisites: Java

Learn this in 10 minutes10 min
  1. Learn the Given / When / Then structure of Gherkin
  2. Write one feature file and match it to step definitions
  3. Use Scenario Outlines and data tables to avoid duplicated scenarios
  4. Read Common Anti-Patterns so scenarios stay readable to non-engineers
After this guide you can
  • Write scenarios in Gherkin
  • Bind steps to step definitions
  • Use scenario outlines, hooks, and tags
  • Avoid the common BDD anti-patterns

๐Ÿงญ Mental modelThe BDD loop from conversation to executable specThe Three Amigos collaborate to write a Gherkin feature file, step definitions bind that plain text to code, execution produces a pass or fail report, and the results feed back into the next round of collaborative refinement.Three Amigosbusiness + dev + QAFeature fileGherkin: Given/When/ThenStep definitionsbinds text to codeTest executionpass / fail reportreview and refine together

BDD starts as a conversation, not code: the Three Amigos agree on behavior in a Gherkin feature file written in Given/When/Then form, step definitions translate that plain English into executable code, and the resulting pass/fail report feeds back into the next round of collaborative refinement, so the feature file stays living documentation instead of a one-time spec.

1. What BDD Is and the Problem It Solvesโ€‹

Behavior-Driven Development is a collaboration practice โ€” not fundamentally a testing framework โ€” that grew out of Test-Driven Development. Its core idea: define expected system behavior in a shared, structured natural language before implementation, so business stakeholders, developers, and testers agree on what "done" means using the exact same document.

Cucumber is the best-known tool that executes BDD specifications written in Gherkin as automated tests โ€” it's the glue between the human-readable spec and the code that verifies it.

The "Three Amigos"โ€‹

The practice of writing scenarios collaboratively with three perspectives in the room before development starts:

RoleBrings
Business/ProductWhat outcome matters and why (the "why")
DevelopmentWhat's technically feasible, edge cases in implementation
Testing/QAWhat could go wrong, missing edge cases, testability concerns

The output of a Three Amigos session is a set of concrete Given/When/Then examples โ€” this is BDD's real mechanism: replacing vague requirements ("the login should work correctly") with unambiguous, testable examples agreed on by all three roles before a line of code is written.

Living documentationโ€‹

Because feature files are plain text, version-controlled alongside the code, and directly executable, they function as living documentation โ€” unlike a requirements doc or wiki page, a Gherkin feature file can never silently drift out of sync with actual system behavior, because a failing scenario is a build failure.


2. Gherkin Syntaxโ€‹

Gherkin is BDD's structured, (mostly) natural-language, line-oriented syntax โ€” parsed by keyword, not free-form English.

Feature: User Login
As a registered user
I want to log in with my credentials
So that I can access my account dashboard

Background:
Given the login page is open

Scenario: Valid credentials grant access
Given a registered user with username "standard_user" and password "secret_sauce"
When the user submits the login form
Then the user should see the dashboard
And the welcome message should contain "standard_user"

Scenario: Invalid password is rejected
Given a registered user with username "standard_user" and password "wrong_pass"
When the user submits the login form
Then the user should see an error message "Invalid credentials"

Keyword referenceโ€‹

KeywordPurpose
FeatureTop-level description of the capability under test; free-text narrative below it is documentation only
BackgroundSteps run before every Scenario in the file โ€” shared setup, avoids repeating the same Given in each scenario
ScenarioOne concrete example/test case
GivenEstablishes the initial context/state
WhenThe action/event under test
ThenThe expected outcome
And / ButContinuation of the previous keyword's type โ€” purely for readability, functionally identical to repeating Given/When/Then
Scenario Outline + ExamplesA templated scenario run once per data row (ยง4)

Gherkin is intentionally declarative, not imperative โ€” it describes what the system does, not how the test interacts with the UI. This distinction is the most common thing separating good Gherkin from bad Gherkin (ยง7).


3. Feature Files & Step Definitionsโ€‹

The feature file is pure Gherkin โ€” no implementation. Each step line is matched at runtime to a step definition: a function whose text pattern or regex matches the step, containing the actual automation code.

# src/test/resources/features/login.feature
Feature: User Login

Scenario: Valid credentials grant access
Given a registered user with username "standard_user" and password "secret_sauce"
When the user submits the login form
Then the user should see the dashboard
// Cucumber-JVM (Java) โ€” Cucumber Expressions (preferred over regex since Cucumber 4+)
public class LoginSteps {

private final LoginPage loginPage = new LoginPage(driver);

@Given("a registered user with username {string} and password {string}")
public void aRegisteredUser(String username, String password) {
loginPage.enterCredentials(username, password);
}

@When("the user submits the login form")
public void theUserSubmitsTheLoginForm() {
loginPage.submit();
}

@Then("the user should see the dashboard")
public void theUserShouldSeeTheDashboard() {
assertTrue(dashboardPage.isDisplayed());
}
}
# behave (Python) โ€” decorator-based step definitions
from behave import given, when, then

@given('a registered user with username "{username}" and password "{password}"')
def step_enter_credentials(context, username, password):
context.login_page.enter_credentials(username, password)

@when('the user submits the login form')
def step_submit(context):
context.login_page.submit()

@then('the user should see the dashboard')
def step_see_dashboard(context):
assert context.dashboard_page.is_displayed()

Cucumber Expressions ({string}, {int}, {word}) are the modern default over raw regex โ€” more readable, and custom parameter types can be registered for domain objects. Regex (^a registered user...$) still works and is common in older codebases.

Matching is text-based, not line-number-basedโ€‹

A step's Given/When/Then prefix is stripped before matching โ€” Cucumber matches only the text after the keyword, which is why And/But steps reuse whatever step definition matches their text regardless of which keyword type defined it originally.


4. Data Tables & Scenario Outlinesโ€‹

Data tables โ€” structured data within a single scenarioโ€‹

Scenario: Cart total reflects multiple items
Given the cart contains the following items:
| name | price | quantity |
| Widget | 9.99 | 2 |
| Gadget | 19.99 | 1 |
When the user views the cart summary
Then the total should be "39.97"
@Given("the cart contains the following items:")
public void theCartContains(io.cucumber.datatable.DataTable table) {
List<Map<String, String>> rows = table.asMaps();
for (Map<String, String> row : rows) {
cart.addItem(row.get("name"), Double.parseDouble(row.get("price")),
Integer.parseInt(row.get("quantity")));
}
}

Scenario Outline + Examples โ€” the same scenario, run per data rowโ€‹

Scenario Outline: Invalid login attempts are rejected
Given a registered user with username "<username>" and password "<password>"
When the user submits the login form
Then the user should see an error message "<error>"

Examples:
| username | password | error |
| standard_user | wrong_pass | Invalid credentials |
| "" | secret_sauce| Username is required |
| locked_out | secret_sauce| Account is locked |

Each Examples row generates and reports as an independent scenario โ€” the direct Gherkin equivalent of TestNG's @DataProvider or JUnit's @ParameterizedTest, expressed in the feature file itself instead of code, which keeps the data visible to non-programmer reviewers.


5. Hooksโ€‹

Hooks run code around scenarios without cluttering the feature file itself โ€” Cucumber's equivalent of TestNG's @Before/@AfterMethod or JUnit's @BeforeEach/@AfterEach.

public class Hooks {

@Before // before every scenario
public void setUp() {
driver = new ChromeDriver();
}

@Before("@api") // only before scenarios tagged @api
public void setUpApiClient() {
apiClient = new ApiClient();
}

@After // after every scenario
public void tearDown(Scenario scenario) {
if (scenario.isFailed()) {
byte[] screenshot = ((TakesScreenshot) driver).getScreenshotAs(OutputType.BYTES);
scenario.attach(screenshot, "image/png", "failure-screenshot");
}
driver.quit();
}

@BeforeStep
public void beforeEachStep() { /* runs before every individual step */ }
}

@Before/@After can be tag-scoped (@Before("@api"), @Before("@ui and not @mobile")) so setup logic only runs for the scenarios that actually need it โ€” critical once a suite mixes UI, API, and mobile scenarios in one project. Execution order across multiple @Before hooks follows declaration order by default, overridable with @Before(order = 1).


6. Tagsโ€‹

@smoke @login
Feature: User Login

@regression
Scenario: Valid credentials grant access
...

@wip @skip
Scenario: Password reset flow
...
mvn test -Dcucumber.filter.tags="@smoke and not @wip"
// JUnit 5 Platform Suite runner
@Suite
@IncludeEngines("cucumber")
@SelectClasspathResource("features")
@ConfigurationParameter(key = FILTER_TAG_PROPERTY_NAME, value = "@smoke and not @wip")
public class RunCucumberTest {}

Tags apply at both Feature and Scenario level (a Feature-level tag applies to every Scenario inside it) and support boolean expressions (and, or, not) for precise CI filtering โ€” same purpose as TestNG groups or JUnit @Tag, but visible directly in the spec that non-programmers read.


7. Cucumber-JVM vs. Other Implementationsโ€‹

ImplementationLanguageNotes
Cucumber-JVMJava, Kotlin, ScalaThe reference/most mature implementation; integrates with JUnit 5 (Platform Suite) or TestNG as the runner
behavePythonSimilar Gherkin support, Python-idiomatic step definitions via decorators
SpecFlow / Reqnroll.NET/C#SpecFlow is now community-maintained as Reqnroll after SpecFlow's commercial sunset โ€” same Gherkin model
Cucumber.jsJavaScript/TypeScriptNode-based, integrates with Playwright/WebdriverIO
GodogGoGherkin support for Go projects

All implementations share the same Gherkin parser/spec (feature files are portable across languages) โ€” only the step definition binding syntax and runner integration differ.

Cucumber-JVM runner setup (JUnit 5 Platform)โ€‹

import org.junit.platform.suite.api.*;
import static io.cucumber.junit.platform.engine.Constants.*;

@Suite
@IncludeEngines("cucumber")
@SelectClasspathResource("features")
@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "com.company.steps")
@ConfigurationParameter(key = PLUGIN_PROPERTY_NAME,
value = "pretty, html:target/cucumber-report.html, json:target/cucumber.json")
public class RunCucumberTest {}
  • glue points Cucumber at the package(s) containing step definitions and hooks.
  • Plugins (pretty, html, json, junit) control report output โ€” html produces a browsable report; json feeds tools like Allure or custom CI dashboards.

8. Common Anti-Patternsโ€‹

Imperative steps (bad) vs. declarative steps (good)โ€‹

# Imperative โ€” describes UI mechanics, brittle, unreadable to non-programmers
Scenario: Login
Given the user navigates to "/login"
When the user types "standard_user" into the field with id "username"
And the user types "secret_sauce" into the field with id "password"
And the user clicks the element with id "login-button"
Then the text "Dashboard" should be visible on the page

# Declarative โ€” describes intent/behavior, resilient to UI changes
Scenario: Login
Given a registered user
When the user logs in with valid credentials
Then the user should see the dashboard

Imperative steps couple the spec to implementation details (a CSS ID change breaks the feature file, not just a page object) and defeat BDD's purpose โ€” a non-programmer can't review "clicks the element with id login-button" as a business requirement. Push the "how" into the step definition/page object; keep the feature file describing "what" and "why."

Other common anti-patternsโ€‹

Anti-patternWhy it hurts
Feature files written solo by QA, never reviewed by the teamDefeats the entire collaboration premise of BDD โ€” becomes just "Gherkin as a test DSL," losing the living-documentation value
One giant Scenario covering an entire user journeyHard to pinpoint failures, hard to reuse steps; prefer small, focused scenarios per behavior
Step definitions with business logic/assertions duplicated across filesSame discipline problem as any codebase โ€” extract shared logic, use page objects/helper classes behind step defs
Over-parameptrizing every noun into Scenario OutlineReduces readability for cases where only 1-2 concrete examples are needed; reserve outlines for genuinely repetitive variations
Using Background for something only half the scenarios needForces unnecessary setup cost/noise on scenarios that don't need it โ€” keep Background truly universal to the feature
Treating Cucumber as "just a test framework"Misses the point โ€” if scenarios aren't written collaboratively pre-development, you've lost BDD's core value and kept only its syntax overhead

9. Interview-Ready Q&Aโ€‹

Q: What problem does BDD actually solve, distinct from just "testing with Gherkin"? A: BDD is a collaboration practice for establishing shared understanding of requirements before development, using concrete Given/When/Then examples agreed on by business, dev, and QA together (the "Three Amigos"). Cucumber executing Gherkin as automated tests is a consequence of that practice, not its point โ€” a team that writes Gherkin solo in QA after the fact and calls it "BDD" has kept the syntax but lost the actual value: shared understanding and living documentation.

Q: What's the difference between imperative and declarative Gherkin steps, and why does it matter? A: Imperative steps describe UI mechanics ("click the element with id login-button"); declarative steps describe intent ("the user logs in with valid credentials"). Imperative steps couple the spec to implementation details, so a CSS/DOM change breaks the feature file itself, and they're unreadable as a business requirement to non-programmers โ€” defeating BDD's purpose. Declarative steps push the "how" into step definitions/page objects and keep the feature file describing "what" and "why."

Q: How does Background differ from a @Before hook? A: Background is Gherkin syntax living inside the feature file โ€” steps that run before every Scenario in that file, visible to anyone reading the spec. A @Before hook is code-level setup (driver initialization, test data seeding) invisible in the feature file itself. Use Background for business-meaningful shared context worth stating explicitly in the spec; use hooks for technical/infrastructure setup that isn't part of the behavior being described.

Q: How would you run only smoke tests from a large Cucumber suite in CI? A: Tag the relevant Features or Scenarios with @smoke, then filter at runtime with a tag expression โ€” -Dcucumber.filter.tags="@smoke and not @wip" via Maven, or the equivalent ConfigurationParameter on the JUnit Platform Suite runner. Tag expressions support boolean logic (and, or, not), so CI stages can compose precise subsets without maintaining separate feature files.

Q: Explain Scenario Outline and when you'd reach for it. A: It's a templated scenario with placeholders (<username>) filled in by each row of an Examples table, and each row runs and reports as an independent scenario. Use it when you have genuinely repetitive variations of the same behavior (multiple invalid-login combinations, boundary values) โ€” it keeps the data visible in the spec itself, which matters for non-programmer reviewers, unlike hiding equivalent data in a code-level data provider.

Q: What's the risk of writing one giant end-to-end Scenario instead of several focused ones? A: A failure anywhere in a long scenario makes it hard to pinpoint what actually broke, since Cucumber reports failure at the scenario level; it also discourages step reuse across scenarios and makes the spec harder for reviewers to read as a discrete behavior. Prefer small, focused scenarios, each verifying one behavior, composed from reusable steps.

Q: How does Cucumber-JVM integrate with JUnit 5 or TestNG as a runner? A: Cucumber itself parses feature files and matches steps via its own engine; JUnit 5's Platform Suite API (@IncludeEngines("cucumber"), @SelectClasspathResource) or a TestNG-based runner acts as the entry point that build tools (Maven Surefire, Gradle) actually invoke, letting Cucumber scenarios show up in the same reports and CI tooling as regular JUnit/TestNG tests.

Q: A step definition text doesn't match its Gherkin step โ€” what typically causes that, and how do you debug it? A: Usually a mismatch between the Cucumber Expression/regex pattern and the literal step text (a typo, an extra/missing quoted argument, or a parameter type mismatch like expecting {int} where the value has quotes). Cucumber's "undefined step" output at runtime actually prints a suggested step definition snippet matching the exact text โ€” the fastest way to spot the mismatch is comparing that generated snippet against your real step definition's pattern.


Practical exercises
  1. Create a Maven project with these test dependencies: io.cucumber:cucumber-java and io.cucumber:cucumber-junit-platform-engine (7.20.x), org.junit.platform:junit-platform-suite, and org.junit.jupiter:junit-jupiter. Add the runner class from the guide's setup section, pointing its glue at your own package. Then add this class and feature file (in src/test/resources/features):

    public class Shipping {
    /** Domestic parcels cost 5 + 2 per kg; international cost 15 + 4 per kg. */
    public static double cost(double kg, String zone) {
    return zone.equals("international") ? 15 + 4 * kg : 5 + 2 * kg;
    }
    }
    Feature: Shipping cost

    @smoke
    Scenario Outline: Cost depends on weight and zone
    Given a parcel weighing <kg> kilograms
    When it is shipped <zone>
    Then the cost is <cost>

    Examples:
    | kg | zone | cost |
    | 1 | domestic | 7.0 |
    | 2 | domestic | 9.0 |
    | 1 | international | 19.0 |

    Write the step definitions using Cucumber Expressions ({double} and {word}), and run mvn test.

    Done when: mvn test reports Tests run: 3 with no failures. Then change one cost in the table to a wrong value and confirm exactly one of the three rows fails while the other two still pass.

    Stretch: Rewrite this imperative scenario declaratively: open /cart, click the element with id checkout, type 4111 into the field with id card, click the element with id pay, expect the text Thank you.

  2. Add a hook that runs only for smoke scenarios, and add these two scenarios to the same feature:

    @Before("@smoke")
    public void smokeSetUp(Scenario scenario) {
    System.out.println("SMOKE HOOK: " + scenario.getName());
    }
      @smoke
    Scenario: Heavy parcels still get a price
    Given a parcel weighing 10 kilograms
    When it is shipped domestic
    Then the cost is 25.0

    @wip @smoke
    Scenario: Same-day delivery is not built yet
    Given a parcel weighing 1 kilograms
    When it is shipped domestic
    Then the cost is 0.0

    Run mvn test, then run it again with a tag expression:

    mvn test -Dcucumber.filter.tags="@smoke and not @wip"

    Done when: the first run executes 5 scenarios, prints the hook line 5 times, and reports 1 failure. The filtered run passes, prints the hook line 4 times, and reports the @wip scenario as skipped.

The feature file only developers could read
Context

Illustrative scenario. A team's feature files spell out every UI action: navigate to a URL, type into the field with a given id, click the element with another id. The product owner stopped reading them months ago.

What happened

A front-end refactor renamed a handful of element ids, and dozens of scenarios broke even though no behaviour had changed. The same steps were copy-pasted across scenarios, so each rename meant editing the feature files themselves instead of one place in the code.

Lesson

Write declarative steps that describe intent, such as "a registered user logs in", and keep the mechanics inside step definitions or page objects. The scenarios stay readable to non-programmers and a UI change touches one class instead of every scenario.

Use this with AI
  • Ask an assistant to turn a user story into Gherkin scenarios, then rewrite any step that mentions UI mechanics (ids, clicks, URLs) into intent, and have a non-programmer read the result.
  • Have it draft step definitions for a feature file, then run the suite and check that unmatched or ambiguous steps are reported instead of silently accepted.
  • Ask it to propose tags such as smoke, regression, and wip for existing scenarios, and verify each tag expression selects the scenarios you expect by counting the results.

10. One-Line Summaryโ€‹

Cucumber's real value is upstream of the code โ€” Gherkin scenarios written collaboratively by business, dev, and QA before implementation create living documentation that stays true by construction; keep steps declarative, reserve Scenario Outline for genuine data variation, and treat Cucumber as a collaboration practice with a test runner attached, not a test runner with a natural-language decorator.