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 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 <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.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.