Back to All Articles
Automation

BDD with Cucumber and Java — Complete Guide

Honnesh Muppala May 5, 2026 15 min read

What is BDD?

Behavior-Driven Development (BDD) is a software development methodology that extends Test-Driven Development by writing test specifications in plain, human-readable language that both technical and non-technical stakeholders can understand. The goal is to bridge the communication gap between product owners, developers, and QA engineers — creating a shared language around expected system behaviour before a single line of implementation code is written.

BDD uses Gherkin — a structured natural language — to describe application behaviour in terms of scenarios. Each scenario describes a specific business rule or user journey using three core keywords:

These Gherkin scenarios serve as living documentation — they are both the specification and the automated test. When the tests pass, the documentation is accurate. When requirements change, the feature file changes, and the documentation is automatically updated.

BDD at Viasat: When I introduced BDD practices at Viasat, the immediate win was not the test automation itself — it was the three-amigos conversations. Before writing a feature file, we brought together the product owner, a developer, and a QA engineer to agree on the expected behaviour in Gherkin. These conversations consistently surfaced ambiguities in requirements before they became bugs. The feature files became the contract between product and engineering.

Architecture Diagram

Feature File
Gherkin / .feature
Cucumber Runner
JUnit 5 Suite
Step Definitions
Java @Given @When @Then
Page Objects
Selenium WebDriver
Browser
Chrome / Firefox
Feature files drive the Cucumber runner, which maps Gherkin steps to Java methods that control the browser.

Maven Dependencies

Add these dependencies to your pom.xml:

<dependencies>
    <!-- Cucumber Java bindings -->
    <dependency>
        <groupId>io.cucumber</groupId>
        <artifactId>cucumber-java</artifactId>
        <version>7.15.0</version>
        <scope>test</scope>
    </dependency>

    <!-- Cucumber JUnit 5 integration -->
    <dependency>
        <groupId>io.cucumber</groupId>
        <artifactId>cucumber-junit-platform-engine</artifactId>
        <version>7.15.0</version>
        <scope>test</scope>
    </dependency>

    <!-- JUnit 5 platform suite -->
    <dependency>
        <groupId>org.junit.platform</groupId>
        <artifactId>junit-platform-suite</artifactId>
        <version>1.10.1</version>
        <scope>test</scope>
    </dependency>

    <!-- Selenium WebDriver -->
    <dependency>
        <groupId>org.seleniumhq.selenium</groupId>
        <artifactId>selenium-java</artifactId>
        <version>4.18.1</version>
    </dependency>

    <!-- WebDriverManager — automatic driver management -->
    <dependency>
        <groupId>io.github.bonigarcia</groupId>
        <artifactId>webdrivermanager</artifactId>
        <version>5.7.0</version>
        <scope>test</scope>
    </dependency>

    <!-- Allure Cucumber reporting -->
    <dependency>
        <groupId>io.qameta.allure</groupId>
        <artifactId>allure-cucumber7-jvm</artifactId>
        <version>2.25.0</version>
        <scope>test</scope>
    </dependency>
</dependencies>

Feature File Syntax

Feature files are plain text with a .feature extension and live in src/test/resources/features/. They describe the expected behaviour in Gherkin:

# src/test/resources/features/login.feature
Feature: User Authentication
  As a registered user
  I want to be able to log in to my account
  So that I can access personalised content

  Background:
    Given the user is on the login page

  @smoke @auth
  Scenario: Successful login with valid credentials
    When the user enters email "user@example.com" and password "SecurePass123"
    And the user clicks the login button
    Then the user should be redirected to the dashboard
    And the welcome message should contain "Welcome"

  @auth @regression
  Scenario: Login fails with invalid password
    When the user enters email "user@example.com" and password "WrongPassword"
    And the user clicks the login button
    Then an error message "Invalid email or password" should be displayed

  @auth @regression
  Scenario: Login fails with empty fields
    When the user clicks the login button
    Then an error message "Email is required" should be displayed

Gherkin keywords

Step Definitions

Step definitions are Java methods annotated with @Given, @When, or @Then. The annotation value is a regex or Cucumber Expression that matches the Gherkin step text. Parameters are extracted from the step and passed to the method:

// src/test/java/steps/LoginSteps.java
package steps;

import io.cucumber.java.en.*;
import org.openqa.selenium.WebDriver;
import pages.LoginPage;
import static org.assertj.core.api.Assertions.assertThat;

public class LoginSteps {

    private final WebDriver driver = DriverManager.getDriver();
    private final LoginPage loginPage = new LoginPage(driver);

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

    // {string} captures a quoted string parameter
    @When("the user enters email {string} and password {string}")
    public void theUserEntersCredentials(String email, String password) {
        loginPage.enterEmail(email);
        loginPage.enterPassword(password);
    }

    @When("the user clicks the login button")
    public void theUserClicksLoginButton() {
        loginPage.clickLogin();
    }

    @Then("the user should be redirected to the dashboard")
    public void theUserShouldBeRedirectedToDashboard() {
        assertThat(driver.getCurrentUrl()).contains("/dashboard");
    }

    // {string} captures the expected text
    @Then("the welcome message should contain {string}")
    public void theWelcomeMessageShouldContain(String expectedText) {
        assertThat(loginPage.getWelcomeText()).contains(expectedText);
    }

    @Then("an error message {string} should be displayed")
    public void anErrorMessageShouldBeDisplayed(String expectedError) {
        assertThat(loginPage.getErrorText()).isEqualTo(expectedError);
    }
}

JUnit 5 Runner

With Cucumber 7 and JUnit 5, the runner is a suite configuration class rather than a test class:

// src/test/java/runner/CucumberRunner.java
package runner;

import org.junit.platform.suite.api.*;

@Suite
@IncludeEngines("cucumber")
@ConfigurationParameter(
    key = "cucumber.features",
    value = "src/test/resources/features"
)
@ConfigurationParameter(
    key = "cucumber.glue",
    value = "steps,hooks"
)
@ConfigurationParameter(
    key = "cucumber.plugin",
    value = "pretty, html:target/cucumber-reports/report.html, " +
            "json:target/cucumber-reports/report.json, " +
            "io.qameta.allure.cucumber7jvm.AllureCucumber7Jvm"
)
@ConfigurationParameter(
    key = "cucumber.filter.tags",
    value = "@smoke or @regression"
)
public class CucumberRunner {
    // This class body intentionally left empty
}

Run the suite with Maven: mvn test -Dtest=CucumberRunner

Scenario Outline for Data-Driven Tests

Scenario Outline combined with an Examples table allows you to run the same scenario multiple times with different input data — replacing hardcoded values with <placeholder> variables:

# features/login.feature
  @data-driven
  Scenario Outline: Login attempts with various credentials
    Given the user is on the login page
    When the user enters email "<email>" and password "<password>"
    And the user clicks the login button
    Then "<outcome>" should be observed

    Examples:
      | email                 | password      | outcome                      |
      | user@example.com      | ValidPass123  | redirected to dashboard      |
      | user@example.com      | WrongPass     | Invalid email or password    |
      | notregistered@ex.com  | AnyPass123    | Invalid email or password    |
      | admin@example.com     | AdminPass456  | redirected to admin panel    |

Cucumber runs this as 4 separate test cases — one per row in the Examples table. Each row is reported individually in the test report, making it easy to see which specific data combination failed.

Data Tables

Data Tables allow you to pass structured multi-row data to a single step — useful for form submissions with many fields or testing batch operations:

# Feature file step with a data table
    When the user fills in the registration form with:
      | field         | value              |
      | First Name    | Alice              |
      | Last Name     | Smith              |
      | Email         | alice@example.com  |
      | Phone         | +353 87 123 4567   |
      | Role          | Administrator      |
// Step definition — receives data as List<Map<String, String>>
import io.cucumber.datatable.DataTable;
import java.util.List;
import java.util.Map;

@When("the user fills in the registration form with:")
public void theUserFillsInRegistrationForm(DataTable dataTable) {
    List<Map<String, String>> rows = dataTable.asMaps(String.class, String.class);
    for (Map<String, String> row : rows) {
        registrationPage.fillField(row.get("field"), row.get("value"));
    }
    registrationPage.submit();
}

Hooks

Hooks run setup and teardown code around scenarios. They live in a dedicated class in the glue package:

// src/test/java/hooks/TestHooks.java
package hooks;

import io.cucumber.java.*;
import io.cucumber.java.Scenario;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.TakesScreenshot;
import org.openqa.selenium.WebDriver;

public class TestHooks {

    private final WebDriver driver = DriverManager.getDriver();

    @Before
    public void setUp(Scenario scenario) {
        System.out.println("Starting scenario: " + scenario.getName());
        // Additional setup: clear cookies, set implicit wait, etc.
        driver.manage().deleteAllCookies();
    }

    @After
    public void tearDown(Scenario scenario) {
        // Capture screenshot on failure and embed in report
        if (scenario.isFailed()) {
            byte[] screenshot = ((TakesScreenshot) driver)
                .getScreenshotAs(OutputType.BYTES);
            scenario.attach(screenshot, "image/png", "Failure Screenshot");
        }
        DriverManager.quitDriver();
    }

    @Before(value = "@admin", order = 10)
    public void loginAsAdmin() {
        // Special setup for admin scenarios
        AuthHelper.loginViaApi("admin@example.com", "AdminPass123");
    }

    @AfterStep
    public void afterEachStep(Scenario scenario) {
        // Optionally capture a screenshot after every step for debugging
        // Only enable this in debug mode to avoid massive report sizes
    }
}
From Experience at Amazon: At Amazon, our Cucumber suite tested the seller portal — a complex, permission-driven interface. The @Before hook that logged in via API (bypassing the UI) was the single most important performance optimisation. The login flow involved OAuth, MFA simulation, and a 5-second redirect chain. By moving this to an API call in the hook, we cut scenario setup time from 8 seconds to under 1 second across 200+ scenarios — saving over 20 minutes per full regression run.

Tags and Filtering

Tags let you categorize scenarios and run subsets from the CLI or CI configuration:

# Feature file tags
@smoke
Scenario: Quick login verification

@regression @auth
Scenario: Password reset flow

@wip
Scenario: New feature under development (excluded from CI)

@slow @performance
Scenario: Concurrent login load test
# Run only @smoke tests
mvn test -Dcucumber.filter.tags="@smoke"

# Run @regression but not @wip
mvn test -Dcucumber.filter.tags="@regression and not @wip"

# Run either @smoke or @critical
mvn test -Dcucumber.filter.tags="@smoke or @critical"

Reporting

Cucumber generates an HTML report natively. For richer reports, integrate Allure:

# Generate Allure report after test run
mvn allure:serve    # Generates and opens in browser

# Or generate static report
mvn allure:report   # Creates target/site/allure-maven-plugin/

The Allure report shows scenario results grouped by feature, with embedded screenshots, step timings, and tag filtering. For CI pipelines, use the JSON output and the Allure GitHub Action to publish the report as a CI artifact.

Full Example: Feature + Step Def + Page Object

# features/checkout.feature
Feature: Product Checkout
  @smoke
  Scenario: Complete checkout with a single product
    Given the user is logged in as "alice@example.com"
    And the user has added "Wireless Headphones" to the cart
    When the user proceeds to checkout
    And the user enters payment details for card "4111111111111111"
    Then the order confirmation page should be displayed
    And a confirmation email should be queued for "alice@example.com"
// pages/CheckoutPage.java
package pages;

import org.openqa.selenium.*;
import org.openqa.selenium.support.*;
import org.openqa.selenium.support.ui.*;

public class CheckoutPage {
    private WebDriver driver;
    private WebDriverWait wait;

    @FindBy(id = "card-number") private WebElement cardNumberInput;
    @FindBy(css = "[data-testid='place-order-btn']") private WebElement placeOrderBtn;
    @FindBy(css = "[data-testid='order-confirmation']") private WebElement confirmationBanner;

    public CheckoutPage(WebDriver driver) {
        this.driver = driver;
        this.wait = new WebDriverWait(driver, Duration.ofSeconds(10));
        PageFactory.initElements(driver, this);
    }

    public void open() {
        driver.get(ConfigReader.getBaseUrl() + "/checkout");
    }

    public void enterCardNumber(String cardNumber) {
        wait.until(ExpectedConditions.visibilityOf(cardNumberInput));
        cardNumberInput.clear();
        cardNumberInput.sendKeys(cardNumber);
    }

    public void clickPlaceOrder() {
        placeOrderBtn.click();
    }

    public boolean isConfirmationDisplayed() {
        return wait.until(ExpectedConditions.visibilityOf(confirmationBanner)).isDisplayed();
    }
}

GitHub Actions CI

# .github/workflows/cucumber.yml
name: Cucumber BDD Tests

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  cucumber:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'
          cache: maven

      - name: Install Chrome
        uses: browser-actions/setup-chrome@latest

      - name: Run Cucumber tests
        run: mvn test -Dcucumber.filter.tags="@smoke or @regression" -Dwebdriver.chrome.headless=true

      - name: Generate Allure report
        if: always()
        run: mvn allure:report

      - name: Upload Allure report
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: allure-report
          path: target/site/allure-maven-plugin/

      - name: Upload Cucumber JSON report
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: cucumber-report
          path: target/cucumber-reports/

BDD Framework Comparison

Feature Cucumber (Java) SpecFlow (.NET) Behave (Python) Robot Framework
Language Java C# / .NET Python Python (keyword-driven)
Gherkin support Full (Feature / Scenario / Outline) Full (same spec) Full (same spec) BDD plugin only
Reporting HTML, JSON, Allure, JUnit HTML, JSON, Allure JSON, Allure, HTML formatter Built-in HTML, LOG files
Learning curve Medium (Java + Maven) Medium (.NET ecosystem) Low (Python) Low (keyword-driven)
IDE support Excellent (IntelliJ plugin) Excellent (Visual Studio) Good (VS Code) Good (RIDE, VS Code)
Community Very large Medium Medium Large
Best for Java enterprise teams .NET / Microsoft shops Python QA teams Acceptance testing, non-dev testers

Best Practices

1. Keep steps business-level, not technical

Gherkin should read like a business requirement, not a test script. Avoid implementation details in your steps:

# Bad — implementation leaking into Gherkin
When I click the element with id "login-btn"

# Good — business intent is clear
When the user submits the login form

2. Write steps to be reusable across scenarios

Design step definitions that work with parameters rather than hard-coding values. A step like the user enters email {string} is reusable across every scenario that involves email input. A step like the user enters email "admin@example.com" is not reusable at all.

3. Keep Given steps free of UI interactions

The Given phase should establish preconditions, not test the process of getting there. Use API calls, direct database inserts, or programmatic setup in Given steps. Reserve UI interaction for When steps.

4. One scenario per business rule

Each scenario should test exactly one outcome. Scenarios that test multiple things are harder to diagnose when they fail — you don't know which assertion caused the failure without reading through the entire scenario.

5. Tag consistently and enforce tag conventions

Define a tag convention that the whole team follows: @smoke for critical path, @regression for full regression, @wip for work in progress (excluded from CI), and @slow for tests that take over 30 seconds. Consistency makes CI tag filtering reliable.


Back to Blog
From Experience — Virtusa: Leading a team of 270 testers at Virtusa, we standardised on Appium for real Android device testing and Selenium WebDriver for web regression. The biggest challenge wasn't the tooling — it was consistency across a team that size. We enforced a strict Page Object Model convention and a pre-merge locator review checklist. Within two sprints, flaky test rates dropped significantly and the team achieved a 20% efficiency gain across regression cycles. At that scale, test architecture decisions matter far more than individual test quality.