Cucumber BDD
Behavior-Driven Development with Gherkin
Introduction to BDD
What is Behavior-Driven Development?
BDD bridges the gap between business stakeholders and technical teams by using a common language to describe application behavior. Cucumber implements BDD using Gherkin syntax.
Project Setup
Add these dependencies to your pom.xml:
<dependencies>
<!-- Cucumber -->
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-java</artifactId>
<version>7.15.0</version>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-testng</artifactId>
<version>7.15.0</version>
</dependency>
<!-- Selenium -->
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>4.16.0</version>
</dependency>
</dependencies>
Project Structure
src/test/
├── java/
│ ├── runners/
│ │ └── TestRunner.java
│ ├── stepdefinitions/
│ │ ├── LoginSteps.java
│ │ └── Hooks.java
│ └── pages/
│ └── LoginPage.java
└── resources/
└── features/
└── login.feature
Gherkin Syntax
Feature File Basics
Feature files use .feature extension and contain scenarios written in Gherkin:
Feature: User Login
As a registered user
I want to login to my account
So that I can access my dashboard
@smoke @login
Scenario: Successful login with valid credentials
Given I am on the login page
When I enter username "testuser"
And I enter password "password123"
And I click the login button
Then I should see the dashboard
And the welcome message should contain "testuser"
Gherkin Keywords
Background - Common Setup
Use Background for steps that repeat in every scenario:
Feature: Shopping Cart
Background:
Given I am logged in as a customer
And I have an empty cart
Scenario: Add single item to cart
When I add "Laptop" to the cart
Then the cart should contain 1 item
Scenario: Add multiple items to cart
When I add "Laptop" to the cart
And I add "Mouse" to the cart
Then the cart should contain 2 items
Step Definitions
Basic Step Definition
Step definitions map Gherkin steps to Java code:
public class LoginSteps {
private WebDriver driver;
private LoginPage loginPage;
@Given("I am on the login page")
public void iAmOnTheLoginPage() {
driver = DriverManager.getDriver();
driver.get("https://example.com/login");
loginPage = new LoginPage(driver);
}
@When("I enter username {string}")
public void iEnterUsername(String username) {
loginPage.enterUsername(username);
}
@When("I enter password {string}")
public void iEnterPassword(String password) {
loginPage.enterPassword(password);
}
@When("I click the login button")
public void iClickTheLoginButton() {
loginPage.clickLogin();
}
@Then("I should see the dashboard")
public void iShouldSeeTheDashboard() {
Assert.assertTrue(loginPage.isDashboardDisplayed());
}
}
Cucumber Expressions
Cucumber 7+ uses Cucumber Expressions (simpler than regex):
// String parameter
@When("I search for {string}")
public void searchFor(String query) { }
// Integer parameter
@Then("I should see {int} results")
public void verifyResults(int count) { }
// Word (no quotes needed)
@Given("I am on the {word} page")
public void navigateToPage(String pageName) { }
// Optional text
@When("I click (the )login button")
public void clickLogin() { }
// Alternative text
@When("I add/remove the item")
public void modifyCart() { }
Custom Parameter Types
@ParameterType("enabled|disabled")
public Boolean status(String status) {
return "enabled".equals(status);
}
// Usage in step
@Then("the feature should be {status}")
public void verifyFeatureStatus(Boolean isEnabled) {
Assert.assertEquals(isEnabled, feature.isEnabled());
}
Hooks
Setup and Teardown
Hooks run before/after scenarios for setup and cleanup:
public class Hooks {
@Before
public void setUp(Scenario scenario) {
System.out.println("Starting: " + scenario.getName());
DriverManager.initDriver();
}
@After
public void tearDown(Scenario scenario) {
// Capture screenshot on failure
if (scenario.isFailed()) {
byte[] screenshot = ((TakesScreenshot) DriverManager.getDriver())
.getScreenshotAs(OutputType.BYTES);
scenario.attach(screenshot, "image/png", "failure-screenshot");
}
DriverManager.quitDriver();
}
}
Hook Types and Order
@BeforeAll // Runs once before all scenarios
public static void globalSetup() {
// Database setup, test data preparation
}
@AfterAll // Runs once after all scenarios
public static void globalTeardown() {
// Cleanup resources
}
@Before(order = 1) // Lower order runs first
public void firstSetup() { }
@Before(order = 2)
public void secondSetup() { }
@Before("@database") // Conditional hook - only for tagged scenarios
public void setupDatabase() { }
@BeforeStep
public void beforeEachStep() { }
@AfterStep
public void afterEachStep(Scenario scenario) {
// Useful for detailed logging
}
Data Tables
Simple Data Table
Scenario: Fill registration form
When I fill in the registration form:
| field | value |
| firstName | John |
| lastName | Doe |
| email | john@example.com |
| phone | 1234567890 |
Then the form should be submitted successfully
@When("I fill in the registration form:")
public void fillRegistrationForm(DataTable dataTable) {
Map<String, String> data = dataTable.asMap(String.class, String.class);
registrationPage.enterFirstName(data.get("firstName"));
registrationPage.enterLastName(data.get("lastName"));
registrationPage.enterEmail(data.get("email"));
registrationPage.enterPhone(data.get("phone"));
}
List of Maps (Multiple Records)
Scenario: Add multiple users
When I create the following users:
| username | email | role |
| john | john@test.com | admin |
| jane | jane@test.com | user |
| bob | bob@test.com | viewer |
@When("I create the following users:")
public void createUsers(DataTable dataTable) {
List<Map<String, String>> users = dataTable.asMaps();
for (Map<String, String> user : users) {
userService.createUser(
user.get("username"),
user.get("email"),
user.get("role")
);
}
}
Transform to Custom Objects
// Using DataTableType transformer
@DataTableType
public User userEntry(Map<String, String> entry) {
return new User(
entry.get("username"),
entry.get("email"),
entry.get("role")
);
}
@When("I create the following users:")
public void createUsers(List<User> users) {
users.forEach(userService::createUser);
}
Scenario Outline
Data-Driven Testing
Run the same scenario with different data sets:
Feature: Login Validation
@regression
Scenario Outline: Login with different credentials
Given I am on the login page
When I enter username "<username>"
And I enter password "<password>"
And I click the login button
Then I should see "<message>"
Examples:
| username | password | message |
| validuser | validpass | Welcome |
| invaliduser | validpass | Invalid credentials |
| validuser | wrongpass | Invalid credentials |
| | validpass | Username required |
| validuser | | Password required |
Multiple Example Tables
Scenario Outline: User permissions by role
Given I am logged in as a "<role>"
When I try to access "<page>"
Then access should be "<access>"
@admin-tests
Examples: Admin Access
| role | page | access |
| admin | settings | granted |
| admin | user-mgmt | granted |
| admin | reports | granted |
@user-tests
Examples: Regular User Access
| role | page | access |
| user | settings | denied |
| user | user-mgmt | denied |
| user | reports | granted |
Test Runner Configuration
TestNG Runner
@CucumberOptions(
features = "src/test/resources/features",
glue = {"stepdefinitions", "hooks"},
plugin = {
"pretty",
"html:target/cucumber-reports/cucumber.html",
"json:target/cucumber-reports/cucumber.json",
"junit:target/cucumber-reports/cucumber.xml",
"rerun:target/failed-scenarios.txt"
},
tags = "@smoke and not @skip",
monochrome = true,
dryRun = false
)
public class TestRunner extends AbstractTestNGCucumberTests {
@Override
@DataProvider(parallel = true)
public Object[][] scenarios() {
return super.scenarios();
}
}
Runner Options Explained
Tag Expressions
// Single tag
tags = "@smoke"
// Multiple tags (AND)
tags = "@smoke and @login"
// Either tag (OR)
tags = "@smoke or @regression"
// Exclude tag
tags = "not @wip"
// Complex expression
tags = "(@smoke or @regression) and not @skip"
Running from Command Line
# Run all tests
mvn test
# Run specific tags
mvn test -Dcucumber.filter.tags="@smoke"
# Run with specific feature
mvn test -Dcucumber.features="src/test/resources/features/login.feature"
# Rerun failed scenarios
mvn test -Dcucumber.features="@target/failed-scenarios.txt"
Page Object Integration
Combining POM with Cucumber
Keep step definitions thin by delegating to Page Objects:
// Page Object
public class LoginPage extends BasePage {
@FindBy(id = "username") private WebElement usernameField;
@FindBy(id = "password") private WebElement passwordField;
@FindBy(id = "login-btn") private WebElement loginButton;
@FindBy(css = ".error-message") private WebElement errorMessage;
public LoginPage(WebDriver driver) {
super(driver);
}
public void login(String username, String password) {
type(usernameField, username);
type(passwordField, password);
click(loginButton);
}
public String getErrorMessage() {
return getText(errorMessage);
}
}
// Step Definition - Clean and focused
public class LoginSteps {
private LoginPage loginPage;
private DashboardPage dashboardPage;
@Given("I am on the login page")
public void navigateToLogin() {
DriverManager.getDriver().get(ConfigReader.getUrl("login"));
loginPage = new LoginPage(DriverManager.getDriver());
}
@When("I login with {string} and {string}")
public void performLogin(String username, String password) {
loginPage.login(username, password);
dashboardPage = new DashboardPage(DriverManager.getDriver());
}
@Then("I should see error {string}")
public void verifyError(String expectedError) {
Assert.assertEquals(expectedError, loginPage.getErrorMessage());
}
}
Sharing State Between Steps
Using Dependency Injection
Share data between step definition classes using PicoContainer:
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-picocontainer</artifactId>
<version>7.15.0</version>
</dependency>
// Shared context class
public class TestContext {
private WebDriver driver;
private Scenario scenario;
private Map<String, Object> scenarioData = new HashMap<>();
public WebDriver getDriver() {
if (driver == null) {
driver = DriverFactory.createDriver();
}
return driver;
}
public void setData(String key, Object value) {
scenarioData.put(key, value);
}
@SuppressWarnings("unchecked")
public <T> T getData(String key) {
return (T) scenarioData.get(key);
}
public void quitDriver() {
if (driver != null) {
driver.quit();
driver = null;
}
}
}
// Step definitions receive shared context via constructor
public class LoginSteps {
private final TestContext context;
private LoginPage loginPage;
public LoginSteps(TestContext context) {
this.context = context;
}
@When("I login as {string}")
public void loginAs(String userType) {
loginPage = new LoginPage(context.getDriver());
User user = TestDataFactory.getUser(userType);
loginPage.login(user.getUsername(), user.getPassword());
context.setData("currentUser", user);
}
}
public class DashboardSteps {
private final TestContext context;
public DashboardSteps(TestContext context) {
this.context = context;
}
@Then("the welcome message should display my name")
public void verifyWelcomeMessage() {
User user = context.getData("currentUser");
DashboardPage dashboard = new DashboardPage(context.getDriver());
Assert.assertTrue(dashboard.getWelcomeMessage().contains(user.getName()));
}
}
Reporting
Built-in Reports
plugin = {
"pretty", // Console output
"html:target/cucumber-reports.html", // HTML report
"json:target/cucumber.json", // JSON for tools
"junit:target/cucumber.xml", // JUnit XML
"timeline:target/timeline" // Timeline report
}
Allure Integration
<dependency>
<groupId>io.qameta.allure</groupId>
<artifactId>allure-cucumber7-jvm</artifactId>
<version>2.25.0</version>
</dependency>
@CucumberOptions(
plugin = {"io.qameta.allure.cucumber7jvm.AllureCucumber7Jvm"}
)
// Add details in steps
@When("I search for {string}")
@Step("Searching for: {query}")
public void search(@Param("query") String query) {
Allure.addAttachment("Search Query", query);
searchPage.search(query);
}
Extent Reports
// In Hooks class
private static ExtentReports extent;
private static ExtentTest feature;
private ExtentTest scenario;
@BeforeAll
public static void initReport() {
extent = new ExtentReports();
extent.attachReporter(new ExtentSparkReporter("reports/extent-report.html"));
}
@Before
public void beforeScenario(Scenario scenario) {
this.scenario = extent.createTest(scenario.getName());
scenario.getSourceTagNames().forEach(tag -> this.scenario.assignCategory(tag));
}
@AfterStep
public void afterStep(Scenario scenario) {
if (scenario.isFailed()) {
this.scenario.fail("Step failed");
}
}
@AfterAll
public static void flushReport() {
extent.flush();
}
Best Practices
Good vs Bad Scenarios
Feature File Organization
features/
├── authentication/
│ ├── login.feature
│ ├── logout.feature
│ └── password-reset.feature
├── shopping/
│ ├── cart.feature
│ ├── checkout.feature
│ └── product-search.feature
└── user-management/
├── profile.feature
└── settings.feature