Cucumber BDD Tutorial

Behaviour-Driven Development (BDD) describes how software should behave in plain language that business people, developers and testers all understand. Cucumber turns those descriptions — written in Gherkin — into automated tests by matching each line to Java code. This tutorial covers the complete path: the BDD mindset, a working Cucumber project, and the framework patterns real teams use, with every in-depth tutorial linked in order.

Advertisement

What Is BDD?

BDD starts with a conversation, not code. Before a story is built, the "three amigos" — a business representative, a developer and a tester — agree on concrete examples of how it should behave. Those examples become scenarios:

Scenario: Successful login
  Given the user is on the login page
  When the user logs in with valid credentials
  Then the dashboard should be displayed

The same scenario is a requirement, an acceptance criterion and an automated test. That shared understanding — not the tool — is the real benefit of BDD.

More: BDD Fundamentals & Gherkin and BDD vs TDD.

How Cucumber Works

  1. You write feature files (.feature) in Gherkin.
  2. You write step definitions — Java methods annotated with @Given, @When, @Then.
  3. A runner tells Cucumber where the features and step definitions (the glue) are.
  4. Cucumber matches each step's text to exactly one step definition and runs it — usually driving Selenium or an API client.

See it step by step in the Cucumber BDD Visualizer.

Set Up a Cucumber Project (Java + TestNG)

Add these to your pom.xml (keep all Cucumber artifacts on the same version):

<dependency>
    <groupId>io.cucumber</groupId>
    <artifactId>cucumber-java</artifactId>
    <version>7.20.1</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>io.cucumber</groupId>
    <artifactId>cucumber-testng</artifactId>
    <version>7.20.1</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>io.cucumber</groupId>
    <artifactId>cucumber-picocontainer</artifactId>
    <!-- shares state between step classes -->
    <version>7.20.1</version>
    <scope>test</scope>
</dependency>

Plus selenium-java for UI tests. A typical layout:

src/test/java/com/example/
├── runners/      → TestRunner
├── steps/        → step definition classes + Hooks
├── pages/        → Page Objects
└── context/      → TestContext (shared state)

src/test/resources/
└── features/     → login.feature, checkout.feature …

Step 1 — A Feature File

@login
Feature: Login

  Background:
    Given the user is on the login page

  @smoke
  Scenario: Successful login
    When the user logs in with "asha@example.com" and "Secret@123"
    Then the dashboard should be displayed

  Scenario Outline: Invalid login shows an error
    When the user logs in with "<email>" and "<password>"
    Then the error "<message>" should be displayed

    Examples:
      | email             | password | message                   |
      | asha@example.com  | wrong    | Invalid email or password |
      |                   | x        | Email is required         |

Everything about Scenario, Scenario Outline, Examples, Background and Data Tables: Feature Files Explained.

Step 2 — Step Definitions

public class LoginSteps {
    private final TestContext context;
    private final LoginPage loginPage;

    public LoginSteps(TestContext context) {          // injected by PicoContainer
        this.context = context;
        this.loginPage = new LoginPage(context.getDriver());
    }

    @Given("the user is on the login page")
    public void onLoginPage() {
        loginPage.open();
    }

    @When("the user logs in with {string} and {string}")
    public void logsIn(String email, String password) {
        loginPage.login(email, password);
    }

    @Then("the error {string} should be displayed")
    public void errorDisplayed(String expected) {
        Assert.assertEquals(loginPage.errorText(), expected);
    }
}

{string} is a Cucumber Expression that captures quoted text. Step definitions stay thin: they call page methods and assert — no locators here.

More: Step Definitions & Cucumber Expressions.

Step 3 — Share State Between Step Classes

Steps for one scenario often live in several classes (login steps, cart steps) that need the same browser. Don't use static fields — let PicoContainer inject one shared object per scenario:

public class TestContext {
    private WebDriver driver;

    public WebDriver getDriver() {
        if (driver == null) driver = new ChromeDriver();
        return driver;
    }

    public void quitDriver() {
        if (driver != null) {
            driver.quit();
            driver = null;
        }
    }
}

Every step class that declares TestContext in its constructor receives the same instance for the current scenario, and a fresh one for the next — which also makes parallel runs safe.

Step 4 — Hooks

public class Hooks {
    private final TestContext context;

    public Hooks(TestContext context) {
        this.context = context;
    }

    @After
    public void tearDown(Scenario scenario) {
        if (scenario.isFailed()) {
            byte[] png = ((TakesScreenshot) context.getDriver())
                    .getScreenshotAs(OutputType.BYTES);

            scenario.attach(png, "image/png", "failure");
            // appears in the report
        }

        context.quitDriver();
    }
}

Hooks hold technical setup and teardown; business preconditions belong in a feature's Background. Use tagged hooks such as @Before("@needsCart") for setup that only some scenarios need.

Step 5 — The Runner and Tags

@CucumberOptions(
    features = "src/test/resources/features",
    glue     = "com.example.steps",            // a package name, not a folder path
    tags     = "@smoke",
    plugin   = {
        "pretty",
        "html:target/cucumber-report.html",
        "json:target/cucumber.json"
    }
)
public class TestRunner extends AbstractTestNGCucumberTests {

    @Override
    @DataProvider(parallel = true)              // run scenarios in parallel
    public Object[][] scenarios() {
        return super.scenarios();
    }
}

Override tags from the command line without touching code:

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

More: Tags & Test Execution.

If steps show up as undefined or ambiguous, see Ambiguous & Undefined Steps.

Step 6 — The Hybrid Framework

Real projects combine Cucumber with the Page Object Model, configuration, test data, reporting and CI:

Feature files → step definitions → page objects → Selenium → browser, with shared context, hooks, config files, and reports published by Jenkins.

Full architecture: BDD Hybrid Framework With POM and The Complete Automation Framework Guide.

When BDD Works — and When It Doesn't

  • Works well when business people actually read or help write scenarios, and features have clear business rules.
  • Struggles when feature files are written by testers alone after development — you get the cost of Gherkin without the collaboration.
  • Common problems: imperative, UI-heavy steps; hundreds of near-duplicate steps; very long scenarios. See BDD Challenges in Real Projects.

The Learning Path

  1. BDD Fundamentals & Gherkin — what BDD is, BDD vs TDD, Gherkin keywords.
  2. Feature Files — Scenario vs Scenario Outline, Examples, Background, Data Tables.
  3. Step Definitions — annotations, parameters, Cucumber Expressions, hooks.
  4. Tags & Execution — tagging levels, the runner, smoke and regression runs.
  5. Hybrid Framework With POM — architecture, data, config and parallel runs.
  6. Real-Project Challenges — step duplication, Gherkin quality and fixes.
  7. Troubleshooting Ambiguous & Undefined Steps
  8. Top 15 BDD & Cucumber Interview Questions

Keep the Cucumber & Gherkin Cheat Sheet handy, and pair this with The Complete Selenium Guide, since Cucumber usually drives Selenium underneath.

How to Use This Guide

  • Beginners: steps 1–5 above, then the learning path in order.
  • Framework builders: shared state, hooks, the runner and the hybrid framework tutorial.
  • Interview prep: Scenario vs Scenario Outline, Background vs hooks, sharing state, tags, and the interview questions.

From Real Projects

BDD and TDD are part of the frameworks I've worked with. What makes BDD valuable is the conversation: scenarios written in plain language that testers, developers and product people all understand, which is also the idea behind Testsigma's plain-English test creation. Keep each scenario focused on one behaviour and let the step definitions call your existing page and business-library code. Write the feature file with the product owner before writing any step definitions.

📚 Official documentation: Cucumber docs: Gherkin reference · Cucumber docs: Step definitions

Cucumber BDD Framework Folder Structure

A Cucumber framework with Java, TestNG and the Page Object Model usually looks like this:

src/test/java/
  runners/TestRunner.java          extends AbstractTestNGCucumberTests
  stepdefinitions/LoginSteps.java  maps Gherkin steps to Java
  hooks/Hooks.java                 @Before / @After: start and quit the browser
  pages/LoginPage.java             page objects: locators + actions
  context/TestContext.java         shares WebDriver between step classes
  utils/ConfigReader.java          reads config.properties
src/test/resources/
  features/login.feature           Gherkin scenarios
  config.properties                base URL, browser, timeouts
pom.xml

The Maven dependencies you need are cucumber-java, cucumber-testng (or cucumber-junit-platform-engine for JUnit 5), cucumber-picocontainer for sharing state, and selenium-java. Keep every io.cucumber artifact on the same version, because mixed versions are the most common cause of "no backend found" and missing-step errors.

The runner ties it together. features points to the feature files and glue lists the packages that hold step definitions and hooks:

@CucumberOptions(
    features = "src/test/resources/features",
    glue = {"stepdefinitions", "hooks"},
    tags = "@smoke",
    plugin = {"pretty", "html:target/cucumber-report.html"}
)
public class TestRunner extends AbstractTestNGCucumberTests { }

Frequently Asked Questions

What is BDD?

Behaviour-Driven Development is a collaborative approach where business, development and QA agree on concrete examples of behaviour, written in Gherkin, which then become automated acceptance tests.

What is Cucumber?

A tool that runs Gherkin feature files by matching each step to a step definition in code. It supports Java, JavaScript, Ruby and other languages.

What is the difference between Scenario and Scenario Outline?

A Scenario runs once with one set of data; a Scenario Outline runs once per row of its Examples table.

What are step definitions?

Java methods annotated with @Given, @When or @Then whose text matches Gherkin steps. They perform the actions and assertions.

How do you share the WebDriver between step definition classes?

With dependency injection — typically PicoContainer — injecting a shared context object into each step class's constructor, rather than using static fields.

How do you run only selected scenarios?

Tag them (@smoke, @regression) and filter with tags in the runner or -Dcucumber.filter.tags on the command line.

How does Cucumber work with the Page Object Model?

Step definitions call page object methods, and page objects contain the locators and Selenium actions — keeping steps readable and changes in one place.

Is Cucumber a testing tool?

It's a tool for executable specifications. It runs tests, but its main value is shared understanding; without collaboration, a plain TestNG framework is often simpler.

Which dependencies does a Cucumber framework need?

For Java with TestNG: cucumber-java, cucumber-testng, cucumber-picocontainer and selenium-java, all managed with Maven. Keep every Cucumber artifact on the same version.

What is the glue option in Cucumber?

glue tells Cucumber which packages contain the step definitions and hooks. If a step shows as undefined even though you wrote it, the package is usually missing from glue.

Can Cucumber run scenarios in parallel?

Yes. With TestNG, override scenarios() in the runner and annotate it with @DataProvider(parallel = true). Each scenario then needs its own WebDriver, which is why a dependency-injection container like PicoContainer is used for shared state.