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.
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 scenariosWriting 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/@Afterare 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 test
# Run a specific feature tag
mvn test -Dcucumber.filter.tags="@smoke"
# Run with a specific glue path
mvn test -Dcucumber.glue="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 test -Dcucumber.filter.tags="@smoke"
# Run smoke AND login
mvn test -Dcucumber.filter.tags="@smoke and @login"
# Exclude slow tests
mvn test -Dcucumber.filter.tags="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.maxBufferand Surefire'sforkCount - Spring Boot integration —
cucumber-springgives 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.