Serenity BDD with Cucumber and JUnit: Complete Setup Guide
Serenity BDD integrates with Cucumber to give you something neither framework provides alone: Cucumber's plain-English scenarios plus Serenity's rich HTML reports with screenshots, step-level narrative, and coverage statistics. This guide covers the full setup from scratch.
What Serenity Adds to Cucumber
Cucumber gives you readable scenarios and step definitions. Serenity adds:
- Narrative HTML reports — not just pass/fail, but step-by-step breakdowns with screenshots at each action
- WebDriver lifecycle management — automatic browser setup, teardown, and screenshot capture
- Screenplay pattern support — a more scalable alternative to page objects
- Requirements coverage — shows which stories and features are tested
- Living documentation — publishable HTML reports that serve as system documentation
Project Setup
Create a Maven project with these dependencies:
<properties>
<serenity.version>3.9.8</serenity.version>
<cucumber.version>7.14.0</cucumber.version>
</properties>
<dependencies>
<!-- Serenity core -->
<dependency>
<groupId>net.serenity-bdd</groupId>
<artifactId>serenity-core</artifactId>
<version>${serenity.version}</version>
<scope>test</scope>
</dependency>
<!-- Serenity + Cucumber integration -->
<dependency>
<groupId>net.serenity-bdd</groupId>
<artifactId>serenity-cucumber</artifactId>
<version>${serenity.version}</version>
<scope>test</scope>
</dependency>
<!-- JUnit 5 runner -->
<dependency>
<groupId>net.serenity-bdd</groupId>
<artifactId>serenity-junit5</artifactId>
<version>${serenity.version}</version>
<scope>test</scope>
</dependency>
<!-- Serenity WebDriver (for UI tests) -->
<dependency>
<groupId>net.serenity-bdd</groupId>
<artifactId>serenity-webdriver</artifactId>
<version>${serenity.version}</version>
<scope>test</scope>
</dependency>
</dependencies>Add the Serenity Maven plugin to generate reports after tests run:
<plugin>
<groupId>net.serenity-bdd.maven.plugins</groupId>
<artifactId>serenity-maven-plugin</artifactId>
<version>${serenity.version}</version>
<executions>
<execution>
<id>serenity-reports</id>
<phase>post-integration-test</phase>
<goals>
<goal>aggregate</goal>
</goals>
</execution>
</executions>
</plugin>Feature Files
Write feature files in src/test/resources/features/:
# src/test/resources/features/user_login.feature
@authentication
Feature: User Login
Background:
Given the login page is open
Scenario: Successful login with valid credentials
When the user enters username "alice@example.com"
And the user enters password "SecurePass123!"
And the user clicks the login button
Then the user should be redirected to the dashboard
And the welcome message should display "Welcome, Alice"
Scenario: Failed login with wrong password
When the user enters username "alice@example.com"
And the user enters password "WrongPassword"
And the user clicks the login button
Then an error message should display "Invalid credentials"
And the user should remain on the login page
Scenario Outline: Login validation
When the user enters username "<email>"
And the user enters password "<password>"
And the user clicks the login button
Then the result should be "<result>"
Examples:
| email | password | result |
| alice@example.com | SecurePass123 | success |
| | SecurePass123 | error |
| alice@example.com | | error |The JUnit 5 Runner
Create a runner class to connect Cucumber with Serenity:
package com.example.tests;
import io.cucumber.junit.platform.engine.Constants;
import net.serenitybdd.cucumber.CucumberWithSerenity;
import org.junit.platform.suite.api.*;
@Suite
@IncludeEngines("cucumber")
@SelectClasspathResource("features")
@ConfigurationParameter(
key = Constants.PLUGIN_PROPERTY_NAME,
value = "io.cucumber.core.plugin.SerenityReporter"
)
@ConfigurationParameter(
key = Constants.GLUE_PROPERTY_NAME,
value = "com.example.steps"
)
public class CucumberTestSuite {}Page Objects with Serenity
Serenity page objects extend PageObject for automatic WebDriver management:
package com.example.pages;
import net.serenitybdd.core.pages.PageObject;
import net.thucydides.core.annotations.DefaultUrl;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
@DefaultUrl("https://example.com/login")
public class LoginPage extends PageObject {
@FindBy(id = "email")
private WebElement emailField;
@FindBy(id = "password")
private WebElement passwordField;
@FindBy(css = "button[type='submit']")
private WebElement loginButton;
@FindBy(css = ".error-message")
private WebElement errorMessage;
@FindBy(css = ".welcome-banner")
private WebElement welcomeBanner;
public void enterEmail(String email) {
emailField.clear();
emailField.sendKeys(email);
}
public void enterPassword(String password) {
passwordField.clear();
passwordField.sendKeys(password);
}
public void clickLogin() {
loginButton.click();
}
public String getErrorMessage() {
return errorMessage.getText();
}
public String getWelcomeMessage() {
return welcomeBanner.getText();
}
public boolean isOnLoginPage() {
return getCurrentUrl().contains("/login");
}
}Step Definitions
Step definition classes inject Serenity page objects via field injection:
package com.example.steps;
import com.example.pages.LoginPage;
import com.example.pages.DashboardPage;
import io.cucumber.java.en.*;
import net.thucydides.core.annotations.Steps;
import org.assertj.core.api.Assertions;
public class LoginSteps {
LoginPage loginPage;
DashboardPage dashboardPage;
@Given("the login page is open")
public void openLoginPage() {
loginPage.open();
}
@When("the user enters username {string}")
public void enterUsername(String username) {
loginPage.enterEmail(username);
}
@When("the user enters password {string}")
public void enterPassword(String password) {
loginPage.enterPassword(password);
}
@When("the user clicks the login button")
public void clickLogin() {
loginPage.clickLogin();
}
@Then("the user should be redirected to the dashboard")
public void verifyDashboardRedirect() {
dashboardPage.shouldBeCurrentlyOpen();
}
@Then("the welcome message should display {string}")
public void verifyWelcomeMessage(String expected) {
Assertions.assertThat(dashboardPage.getWelcomeMessage())
.isEqualTo(expected);
}
@Then("an error message should display {string}")
public void verifyErrorMessage(String expected) {
Assertions.assertThat(loginPage.getErrorMessage())
.isEqualTo(expected);
}
@Then("the user should remain on the login page")
public void verifyStillOnLoginPage() {
Assertions.assertThat(loginPage.isOnLoginPage()).isTrue();
}
}Serenity Configuration
Create serenity.conf in src/test/resources/:
serenity {
project.name = "My Application Tests"
take.screenshots = FOR_FAILURES # or AFTER_EACH_STEP
reports.show.step.details = true
}
webdriver {
driver = chrome
autodownload = true
}
headless.mode = true
environments {
default {
webdriver.base.url = "https://staging.example.com"
}
production {
webdriver.base.url = "https://example.com"
}
}Switch environments at runtime:
mvn verify -Denvironment=productionRunning Tests
# Run all tests
mvn verify
# Run specific tag
mvn verify -Dcucumber.filter.tags="@authentication"
# Run with specific browser
mvn verify -Dwebdriver.driver=firefox
# Headless mode
mvn verify -Dheadless=trueReports appear in target/site/serenity/index.html.
Step Libraries
For reusable actions across multiple step definition classes, create a step library:
package com.example.steps;
import com.example.pages.LoginPage;
import net.thucydides.core.annotations.Step;
public class AuthenticationSteps {
LoginPage loginPage;
@Step("Open the login page and sign in as {0}")
public void loginAs(String email, String password) {
loginPage.open();
loginPage.enterEmail(email);
loginPage.enterPassword(password);
loginPage.clickLogin();
}
@Step("Verify the user is authenticated")
public boolean isLoggedIn() {
return !loginPage.isOnLoginPage();
}
}Inject step libraries with @Steps:
@Steps
AuthenticationSteps auth;
@Given("the user is logged in as {string}")
public void userIsLoggedIn(String email) {
auth.loginAs(email, "defaultPassword");
}The @Step annotation makes each method appear as a named step in Serenity's HTML report.
Hooks and Background Setup
Serenity integrates with Cucumber hooks:
package com.example.steps;
import io.cucumber.java.After;
import io.cucumber.java.Before;
import net.thucydides.core.annotations.Managed;
import org.openqa.selenium.WebDriver;
public class Hooks {
@Managed
WebDriver driver;
@Before("@requires-login")
public void loginBeforeTest() {
// Perform login setup
}
@After
public void cleanupAfterTest(io.cucumber.java.Scenario scenario) {
if (scenario.isFailed()) {
// Serenity already captures screenshots on failure
// Add any additional cleanup here
}
}
}CI Integration
# GitHub Actions
name: Serenity BDD Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Run Serenity tests
run: mvn verify -Dheadless=true
- name: Upload Serenity reports
uses: actions/upload-artifact@v3
if: always()
with:
name: serenity-reports
path: target/site/serenity/Production Monitoring
Serenity + Cucumber covers acceptance testing in your development pipeline. For ongoing production confidence, HelpMeTest complements this by running your critical user flows continuously — without requiring a Java build, WebDriver, or CI trigger. Use Serenity to specify and verify behavior during development; use HelpMeTest to confirm that production stays healthy between releases.
Summary
Serenity BDD + Cucumber + JUnit gives you readable Gherkin scenarios, automatic WebDriver management, and publishable HTML reports — all from the same test run. The setup is straightforward: dependencies, a runner class, page objects, and step definitions. Once wired together, every test run produces a living document of what your system does and whether it does it correctly.