Cucumber + Java: Getting Started with BDD Testing

Cucumber + Java: Getting Started with BDD Testing

Cucumber is one of the most widely adopted BDD frameworks, and Java remains its most popular language host. The combination gives teams a way to write tests in plain English that non-technical stakeholders can read while developers implement real automated browser and API tests underneath.

This guide walks you through setting up Cucumber with Java from zero — creating a Maven project, writing your first .feature file, implementing step definitions, and running tests with JUnit 5.

What You'll Build

By the end of this guide you'll have:

  • A working Maven project with Cucumber 7 + JUnit 5
  • A Gherkin feature file describing login behaviour
  • Step definitions that use Selenium to drive a browser
  • A CI-ready test runner configuration

Prerequisites

  • JDK 11 or later
  • Maven 3.8+
  • Basic Java knowledge
  • Chrome browser installed

Project Setup

Create a new Maven project and add these dependencies to pom.xml:

<dependencies>
  <!-- Cucumber -->
  <dependency>
    <groupId>io.cucumber</groupId>
    <artifactId>cucumber-java</artifactId>
    <version>7.15.0</version>
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>io.cucumber</groupId>
    <artifactId>cucumber-junit-platform-engine</artifactId>
    <version>7.15.0</version>
    <scope>test</scope>
  </dependency>

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

  <!-- Selenium -->
  <dependency>
    <groupId>org.seleniumhq.selenium</groupId>
    <artifactId>selenium-java</artifactId>
    <version>4.18.1</version>
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>io.github.bonigarcia</groupId>
    <artifactId>webdrivermanager</artifactId>
    <version>5.7.0</version>
    <scope>test</scope>
  </dependency>
</dependencies>

<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-surefire-plugin</artifactId>
      <version>3.2.2</version>
    </plugin>
  </plugins>
</build>

Directory Structure

Cucumber expects feature files and step definitions in specific locations:

src/
└── test/
    ├── java/
    │   └── com/example/
    │       ├── RunCucumberTest.java      ← test runner
    │       └── steps/
    │           └── LoginSteps.java       ← step definitions
    └── resources/
        └── features/
            └── login.feature             ← Gherkin scenarios

Writing Your First Feature File

Create src/test/resources/features/login.feature:

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

  Background:
    Given the browser is open at the login page

  Scenario: Successful login with valid credentials
    When I enter username "alice@example.com" and password "secret123"
    And I click the login button
    Then I should see the dashboard page
    And I should see a welcome message for "alice"

  Scenario: Login fails with wrong password
    When I enter username "alice@example.com" and password "wrongpassword"
    And I click the login button
    Then I should see an error message "Invalid credentials"
    And I should remain on the login page

  Scenario Outline: Login with multiple test accounts
    When I enter username "<email>" and password "<password>"
    And I click the login button
    Then I should see the dashboard page

    Examples:
      | email              | password  |
      | alice@example.com  | secret123 |
      | bob@example.com    | pass456   |
      | carol@example.com  | pw789     |

Three things to notice:

  • Feature gives the business context
  • Background runs before every scenario (like @BeforeEach)
  • Scenario Outline with Examples runs the same scenario with multiple data sets

Implementing Step Definitions

Create src/test/java/com/example/steps/LoginSteps.java:

package com.example.steps;

import io.cucumber.java.After;
import io.cucumber.java.Before;
import io.cucumber.java.en.*;
import io.github.bonigarcia.wdm.WebDriverManager;
import org.openqa.selenium.*;
import org.openqa.selenium.chrome.*;
import org.openqa.selenium.support.ui.*;

import static org.assertj.core.api.Assertions.assertThat;

public class LoginSteps {

    private WebDriver driver;
    private static final String BASE_URL = "https://your-app.example.com";

    @Before
    public void setUp() {
        WebDriverManager.chromedriver().setup();
        ChromeOptions options = new ChromeOptions();
        options.addArguments("--headless", "--no-sandbox", "--disable-dev-shm-usage");
        driver = new ChromeDriver(options);
        driver.manage().window().maximize();
    }

    @After
    public void tearDown() {
        if (driver != null) {
            driver.quit();
        }
    }

    @Given("the browser is open at the login page")
    public void openLoginPage() {
        driver.get(BASE_URL + "/login");
        WebDriverWait wait = new WebDriverWait(driver, java.time.Duration.ofSeconds(10));
        wait.until(ExpectedConditions.presenceOfElementLocated(By.id("email")));
    }

    @When("I enter username {string} and password {string}")
    public void enterCredentials(String username, String password) {
        driver.findElement(By.id("email")).sendKeys(username);
        driver.findElement(By.id("password")).sendKeys(password);
    }

    @And("I click the login button")
    public void clickLogin() {
        driver.findElement(By.cssSelector("button[type='submit']")).click();
        // Wait for navigation
        new WebDriverWait(driver, java.time.Duration.ofSeconds(10))
            .until(driver -> !driver.getCurrentUrl().endsWith("/login") ||
                   driver.findElements(By.cssSelector(".error-message")).size() > 0);
    }

    @Then("I should see the dashboard page")
    public void verifyDashboard() {
        assertThat(driver.getCurrentUrl()).contains("/dashboard");
        assertThat(driver.getTitle()).containsIgnoringCase("dashboard");
    }

    @And("I should see a welcome message for {string}")
    public void verifyWelcomeMessage(String name) {
        WebElement welcome = driver.findElement(By.cssSelector(".welcome-message"));
        assertThat(welcome.getText()).containsIgnoringCase(name);
    }

    @Then("I should see an error message {string}")
    public void verifyErrorMessage(String expectedError) {
        WebElement error = new WebDriverWait(driver, java.time.Duration.ofSeconds(5))
            .until(ExpectedConditions.visibilityOfElementLocated(
                By.cssSelector(".error-message")));
        assertThat(error.getText()).contains(expectedError);
    }

    @And("I should remain on the login page")
    public void verifyStillOnLoginPage() {
        assertThat(driver.getCurrentUrl()).endsWith("/login");
    }
}

Key patterns here:

  • {string} in step patterns matches quoted strings in the scenario and injects them as method parameters
  • @Before / @After are Cucumber lifecycle hooks (not JUnit annotations)
  • Each step annotation (@Given, @When, @Then, @And) is interchangeable — use what reads naturally

Test Runner

Create src/test/java/com/example/RunCucumberTest.java:

package com.example;

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

@Suite
@IncludeEngines("cucumber")
@SelectClasspathResource("features")
@ConfigurationParameter(
    key = "cucumber.plugin",
    value = "pretty, html:target/cucumber-reports/report.html, json:target/cucumber-reports/report.json"
)
@ConfigurationParameter(
    key = "cucumber.publish.quiet",
    value = "true"
)
public class RunCucumberTest {}

Running Tests

# Run all tests
mvn <span class="hljs-built_in">test

<span class="hljs-comment"># Run a specific feature tag
mvn <span class="hljs-built_in">test -Dcucumber.filter.tags=<span class="hljs-string">"@smoke"

<span class="hljs-comment"># Run with a specific glue path
mvn <span class="hljs-built_in">test -Dcucumber.glue=<span class="hljs-string">"com.example.steps"

Using Tags to Organize Scenarios

Tags let you run subsets of tests:

@smoke @login
Scenario: Successful login with valid credentials
  ...

@regression
Scenario: Login fails with wrong password
  ...
# Run smoke tests only
mvn <span class="hljs-built_in">test -Dcucumber.filter.tags=<span class="hljs-string">"@smoke"

<span class="hljs-comment"># Run smoke AND login
mvn <span class="hljs-built_in">test -Dcucumber.filter.tags=<span class="hljs-string">"@smoke and @login"

<span class="hljs-comment"># Exclude slow tests
mvn <span class="hljs-built_in">test -Dcucumber.filter.tags=<span class="hljs-string">"not @slow"

HTML Reports

After running tests, open target/cucumber-reports/report.html in your browser. You'll see a colour-coded summary of passed, failed, and skipped scenarios with step-level detail.

For richer reports, add the Masterthought plugin to pom.xml:

<plugin>
  <groupId>net.masterthought</groupId>
  <artifactId>maven-cucumber-reporting</artifactId>
  <version>5.8.0</version>
  <executions>
    <execution>
      <phase>verify</phase>
      <goals><goal>generate</goal></goals>
      <configuration>
        <projectName>My App</projectName>
        <outputDirectory>${project.build.directory}/cucumber-reports/advanced</outputDirectory>
        <jsonFiles>
          <param>${project.build.directory}/cucumber-reports/report.json</param>
        </jsonFiles>
      </configuration>
    </execution>
  </executions>
</plugin>

Common Mistakes

Step not found: Cucumber throws UndefinedStepException if it cannot match a step to a definition. Check that your regex or expression matches the Gherkin text exactly (including quotes).

Shared state between steps: Steps in the same scenario share a step definition class instance by default. For complex state use PicoContainer for dependency injection:

<dependency>
  <groupId>io.cucumber</groupId>
  <artifactId>cucumber-picocontainer</artifactId>
  <version>7.15.0</version>
  <scope>test</scope>
</dependency>
// Shared state class
public class TestContext {
    public WebDriver driver;
    public String currentUser;
}

// Injected into step classes
public class LoginSteps {
    private final TestContext ctx;
    public LoginSteps(TestContext ctx) { this.ctx = ctx; }
}

Flaky waits: Never use Thread.sleep(). Always use WebDriverWait with ExpectedConditions or write a custom FluentWait.

CI Integration

Add to .github/workflows/test.yml:

- name: Run Cucumber tests
  run: mvn test
  env:
    DISPLAY: :99

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

What's Next

With a working Cucumber + Java setup, you can explore:

  • Page Object Model — encapsulate selectors in page classes so step definitions stay readable
  • API testing — use RestAssured in step definitions alongside Selenium for full-stack BDD
  • Parallel execution — configure junit.platform.output.capture.maxBuffer and Surefire's forkCount
  • Spring Boot integrationcucumber-spring gives you the full application context in tests

Cucumber's value isn't just automation — it's the shared language between developers, QA engineers, and product managers that makes requirements testable by design.

Read more