Introduction to BDD and Cucumber
Behavior-Driven Development (BDD) is an agile software development process that encourages collaboration between developers, QA, and non-technical participants. Cucumber is a tool that supports BDD by allowing the execution of feature documentation written in plain text.
Setup and Dependencies
Maven Dependencies
<properties>
<cucumber.version>7.15.0</cucumber.version>
</properties>
<dependencies>
<!-- Cucumber Java -->
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-java</artifactId>
<version>${cucumber.version}</version>
<scope>test</scope>
</dependency>
<!-- Cucumber JUnit -->
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-junit</artifactId>
<version>${cucumber.version}</version>
<scope>test</scope>
</dependency>
<!-- Cucumber Spring (if using Spring) -->
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-spring</artifactId>
<version>${cucumber.version}</version>
<scope>test</scope>
</dependency>
<!-- AssertJ for fluent assertions -->
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.24.2</version>
<scope>test</scope>
</dependency>
<!-- JUnit -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.1.2</version>
</plugin>
</plugins>
</build>
Project Structure
src/ ├── main/ │ └── java/ │ └── com/example/ │ ├── model/ │ ├── service/ │ └── controller/ └── test/ ├── java/ │ └── com/example/ │ ├── runner/ │ ├── stepdefs/ │ └── config/ └── resources/ └── features/
Basic Cucumber Implementation
Feature Files
src/test/resources/features/login/login.feature
Feature: User Login As a user I want to login to the application So that I can access my account Background: Given the application is running And the following users exist: | username | password | status | | john | pass123 | ACTIVE | | jane | pass456 | LOCKED | Scenario: Successful login with valid credentials When I navigate to the login page And I enter username "john" and password "pass123" And I click the login button Then I should be redirected to the dashboard And I should see a welcome message "Welcome, john!" Scenario: Failed login with invalid password When I navigate to the login page And I enter username "john" and password "wrongpass" And I click the login button Then I should see an error message "Invalid credentials" And I should remain on the login page Scenario: Failed login with locked account When I navigate to the login page And I enter username "jane" and password "pass456" And I click the login button Then I should see an error message "Account is locked" And I should remain on the login page Scenario Outline: Login with multiple test data When I navigate to the login page And I enter username "<username>" and password "<password>" And I click the login button Then I should see "<expected_message>" Examples: | username | password | expected_message | | john | pass123 | Welcome, john! | | john | wrong | Invalid credentials | | unknown | pass123 | Invalid credentials |
Step Definitions
src/test/java/com/example/stepdefs/LoginStepDefinitions.java
package com.example.stepdefs;
import io.cucumber.java.Before;
import io.cucumber.java.en.Given;
import io.cucumber.java.en.When;
import io.cucumber.java.en.Then;
import io.cucumber.datatable.DataTable;
import org.assertj.core.api.Assertions;
import com.example.model.User;
import com.example.service.LoginService;
import com.example.service.AuthResult;
import java.util.List;
import java.util.Map;
public class LoginStepDefinitions {
private LoginService loginService;
private AuthResult authResult;
private String currentPage;
private String errorMessage;
@Before
public void setUp() {
loginService = new LoginService();
authResult = null;
currentPage = "login";
errorMessage = null;
}
@Given("the application is running")
public void the_application_is_running() {
// Initialize application context
System.out.println("Application is running...");
}
@Given("the following users exist:")
public void the_following_users_exist(DataTable dataTable) {
List<Map<String, String>> users = dataTable.asMaps();
for (Map<String, String> user : users) {
User newUser = new User(
user.get("username"),
user.get("password"),
user.get("status")
);
loginService.addUser(newUser);
}
}
@When("I navigate to the login page")
public void i_navigate_to_the_login_page() {
currentPage = "login";
System.out.println("Navigated to login page");
}
@When("I enter username {string} and password {string}")
public void i_enter_username_and_password(String username, String password) {
System.out.println("Entering username: " + username + " and password: " + password);
// In real implementation, this would set values in the UI
}
@When("I click the login button")
public void i_click_the_login_button() {
// Simulate login attempt
try {
authResult = loginService.authenticate(getCurrentUsername(), getCurrentPassword());
if (authResult.isSuccess()) {
currentPage = "dashboard";
} else {
errorMessage = authResult.getMessage();
}
} catch (Exception e) {
errorMessage = e.getMessage();
}
}
@Then("I should be redirected to the dashboard")
public void i_should_be_redirected_to_the_dashboard() {
Assertions.assertThat(currentPage)
.isEqualTo("dashboard")
.withFailMessage("Expected to be on dashboard, but was on: " + currentPage);
}
@Then("I should see a welcome message {string}")
public void i_should_see_a_welcome_message(String expectedMessage) {
Assertions.assertThat(authResult.getMessage())
.isEqualTo(expectedMessage);
}
@Then("I should see an error message {string}")
public void i_should_see_an_error_message(String expectedErrorMessage) {
Assertions.assertThat(errorMessage)
.isEqualTo(expectedErrorMessage);
}
@Then("I should remain on the login page")
public void i_should_remain_on_the_login_page() {
Assertions.assertThat(currentPage)
.isEqualTo("login");
}
// Helper methods to simulate UI state
private String getCurrentUsername() {
// In real implementation, this would get from UI state
return "john"; // Simplified for example
}
private String getCurrentPassword() {
// In real implementation, this would get from UI state
return "pass123"; // Simplified for example
}
}
Test Runner
src/test/java/com/example/runner/CucumberTestRunner.java
package com.example.runner;
import io.cucumber.junit.Cucumber;
import io.cucumber.junit.CucumberOptions;
import org.junit.runner.RunWith;
@RunWith(Cucumber.class)
@CucumberOptions(
features = "src/test/resources/features",
glue = {"com.example.stepdefs", "com.example.config"},
plugin = {
"pretty",
"html:target/cucumber-reports/cucumber.html",
"json:target/cucumber-reports/cucumber.json",
"junit:target/cucumber-reports/cucumber.xml"
},
monochrome = true,
tags = "@Login and not @WIP"
)
public class CucumberTestRunner {
}
Domain Models and Services
src/main/java/com/example/model/User.java
package com.example.model;
public class User {
private String username;
private String password;
private String status;
public User(String username, String password, String status) {
this.username = username;
this.password = password;
this.status = status;
}
// Getters and setters
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
@Override
public String toString() {
return "User{username='" + username + "', status='" + status + "'}";
}
}
src/main/java/com/example/service/AuthResult.java
package com.example.service;
public class AuthResult {
private boolean success;
private String message;
private User user;
public AuthResult(boolean success, String message, User user) {
this.success = success;
this.message = message;
this.user = user;
}
public AuthResult(boolean success, String message) {
this(success, message, null);
}
// Getters
public boolean isSuccess() { return success; }
public String getMessage() { return message; }
public User getUser() { return user; }
// Static factory methods
public static AuthResult success(User user) {
return new AuthResult(true, "Welcome, " + user.getUsername() + "!", user);
}
public static AuthResult failure(String message) {
return new AuthResult(false, message);
}
}
src/main/java/com/example/service/LoginService.java
package com.example.service;
import com.example.model.User;
import java.util.HashMap;
import java.util.Map;
public class LoginService {
private Map<String, User> users = new HashMap<>();
public void addUser(User user) {
users.put(user.getUsername(), user);
}
public AuthResult authenticate(String username, String password) {
User user = users.get(username);
if (user == null) {
return AuthResult.failure("Invalid credentials");
}
if ("LOCKED".equals(user.getStatus())) {
return AuthResult.failure("Account is locked");
}
if (!user.getPassword().equals(password)) {
return AuthResult.failure("Invalid credentials");
}
return AuthResult.success(user);
}
public User getUser(String username) {
return users.get(username);
}
public void clearUsers() {
users.clear();
}
}
Advanced Cucumber Features
Data Tables and Transformations
src/test/resources/features/registration/user_registration.feature
Feature: User Registration As a new user I want to register for an account So that I can use the application Scenario: Successful user registration Given I am on the registration page When I fill the registration form with: | Field | Value | | First Name | John | | Last Name | Doe | | Email | [email protected] | | Password | SecurePass123! | | Phone | 1234567890 | And I accept the terms and conditions And I submit the registration form Then I should see a success message "Registration successful" And I should receive a confirmation email And the following user should be created in the database: | Field | Value | | First Name | John | | Last Name | Doe | | Email | [email protected] | | Status | PENDING | Scenario: Registration with invalid email Given I am on the registration page When I fill the registration form with: | Field | Value | | First Name | Jane | | Last Name | Smith | | Email | invalid-email | | Password | SecurePass123! | And I submit the registration form Then I should see an error message "Invalid email format" And I should remain on the registration page
src/test/java/com/example/stepdefs/RegistrationStepDefinitions.java
package com.example.stepdefs;
import io.cucumber.java.en.Given;
import io.cucumber.java.en.When;
import io.cucumber.java.en.Then;
import io.cucumber.datatable.DataTable;
import org.assertj.core.api.Assertions;
import com.example.model.RegistrationForm;
import com.example.service.RegistrationService;
import com.example.model.User;
import java.util.Map;
public class RegistrationStepDefinitions {
private RegistrationService registrationService;
private RegistrationForm currentForm;
private String currentPage;
private String resultMessage;
private boolean termsAccepted;
@Given("I am on the registration page")
public void i_am_on_the_registration_page() {
currentPage = "registration";
currentForm = new RegistrationForm();
termsAccepted = false;
registrationService = new RegistrationService();
}
@When("I fill the registration form with:")
public void i_fill_the_registration_form_with(DataTable dataTable) {
Map<String, String> formData = dataTable.asMap();
currentForm.setFirstName(formData.get("First Name"));
currentForm.setLastName(formData.get("Last Name"));
currentForm.setEmail(formData.get("Email"));
currentForm.setPassword(formData.get("Password"));
currentForm.setPhone(formData.get("Phone"));
System.out.println("Filled form: " + currentForm);
}
@When("I accept the terms and conditions")
public void i_accept_the_terms_and_conditions() {
termsAccepted = true;
}
@When("I submit the registration form")
public void i_submit_the_registration_form() {
if (!termsAccepted) {
resultMessage = "Please accept terms and conditions";
return;
}
try {
User registeredUser = registrationService.registerUser(currentForm);
resultMessage = "Registration successful";
currentPage = "success";
} catch (IllegalArgumentException e) {
resultMessage = e.getMessage();
}
}
@Then("I should see a success message {string}")
public void i_should_see_a_success_message(String expectedMessage) {
Assertions.assertThat(resultMessage).isEqualTo(expectedMessage);
}
@Then("I should see an error message {string}")
public void i_should_see_an_error_message(String expectedMessage) {
Assertions.assertThat(resultMessage).isEqualTo(expectedMessage);
}
@Then("I should remain on the registration page")
public void i_should_remain_on_the_registration_page() {
Assertions.assertThat(currentPage).isEqualTo("registration");
}
@Then("I should receive a confirmation email")
public void i_should_receive_a_confirmation_email() {
// In real implementation, verify email was sent
System.out.println("Confirmation email sent to: " + currentForm.getEmail());
}
@Then("the following user should be created in the database:")
public void the_following_user_should_be_created_in_the_database(DataTable dataTable) {
Map<String, String> expectedData = dataTable.asMap();
User user = registrationService.findUserByEmail(expectedData.get("Email"));
Assertions.assertThat(user).isNotNull();
Assertions.assertThat(user.getFirstName()).isEqualTo(expectedData.get("First Name"));
Assertions.assertThat(user.getLastName()).isEqualTo(expectedData.get("Last Name"));
Assertions.assertThat(user.getEmail()).isEqualTo(expectedData.get("Email"));
Assertions.assertThat(user.getStatus()).isEqualTo(expectedData.get("Status"));
}
}
Custom Data Types and Transformers
src/test/java/com/example/stepdefs/transforms/UserTransformer.java
package com.example.stepdefs.transforms;
import io.cucumber.java.DataTableType;
import com.example.model.User;
import java.util.Map;
public class UserTransformer {
@DataTableType
public User userEntry(Map<String, String> entry) {
return new User(
entry.get("username"),
entry.get("password"),
entry.get("status")
);
}
@DataTableType
public RegistrationForm registrationFormEntry(Map<String, String> entry) {
RegistrationForm form = new RegistrationForm();
form.setFirstName(entry.get("First Name"));
form.setLastName(entry.get("Last Name"));
form.setEmail(entry.get("Email"));
form.setPassword(entry.get("Password"));
form.setPhone(entry.get("Phone"));
return form;
}
}
Hooks and Background Setup
src/test/java/com/example/stepdefs/Hooks.java
package com.example.stepdefs;
import io.cucumber.java.After;
import io.cucumber.java.Before;
import io.cucumber.java.Scenario;
import com.example.service.DatabaseService;
import com.example.service.EmailService;
public class Hooks {
private DatabaseService databaseService;
private EmailService emailService;
@Before(order = 10)
public void setUp(Scenario scenario) {
System.out.println("Starting scenario: " + scenario.getName());
System.out.println("Tags: " + scenario.getSourceTagNames());
databaseService = new DatabaseService();
emailService = new EmailService();
// Clear previous test data
databaseService.clearTestData();
emailService.clearSentEmails();
}
@Before(value = "@Database", order = 20)
public void setupDatabase() {
System.out.println("Setting up database for database-related tests");
databaseService.initializeTestSchema();
}
@Before(value = "@Email", order = 20)
public void setupEmailService() {
System.out.println("Setting up email service for email-related tests");
emailService.startTestMode();
}
@After(order = 10)
public void tearDown(Scenario scenario) {
if (scenario.isFailed()) {
System.out.println("Scenario failed: " + scenario.getName());
// Take screenshot or log additional info
takeScreenshot(scenario);
}
System.out.println("Finished scenario: " + scenario.getName());
}
@After(value = "@Cleanup", order = 5)
public void cleanupTestData() {
System.out.println("Performing additional cleanup");
databaseService.cleanup();
emailService.cleanup();
}
private void takeScreenshot(Scenario scenario) {
// In real implementation, take screenshot for UI tests
System.out.println("Taking screenshot for failed scenario: " + scenario.getName());
// scenario.embed(screenshotBytes, "image/png", scenario.getName());
}
}
Spring Integration
Spring Configuration for Cucumber
src/test/java/com/example/config/CucumberSpringConfiguration.java
package com.example.config;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;
import io.cucumber.spring.CucumberContextConfiguration;
@CucumberContextConfiguration
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ContextConfiguration
public class CucumberSpringConfiguration {
// Spring configuration will be automatically loaded
}
Spring-Based Step Definitions
src/test/java/com/example/stepdefs/SpringLoginStepDefinitions.java
package com.example.stepdefs;
import io.cucumber.java.en.Given;
import io.cucumber.java.en.When;
import io.cucumber.java.en.Then;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.ResponseEntity;
import org.assertj.core.api.Assertions;
import com.example.model.User;
import com.example.repository.UserRepository;
public class SpringLoginStepDefinitions {
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private UserRepository userRepository;
private ResponseEntity<String> response;
private String authToken;
@Given("a user exists with username {string} and password {string}")
public void a_user_exists_with_username_and_password(String username, String password) {
User user = new User(username, password, "ACTIVE");
userRepository.save(user);
}
@When("I send a login request with username {string} and password {string}")
public void i_send_a_login_request_with_username_and_password(String username, String password) {
String loginUrl = "/api/auth/login";
LoginRequest loginRequest = new LoginRequest(username, password);
response = restTemplate.postForEntity(loginUrl, loginRequest, String.class);
if (response.getStatusCode().is2xxSuccessful()) {
// Extract token from response
authToken = response.getBody();
}
}
@Then("the response status should be {int}")
public void the_response_status_should_be(Integer expectedStatus) {
Assertions.assertThat(response.getStatusCodeValue())
.isEqualTo(expectedStatus);
}
@Then("I should receive an authentication token")
public void i_should_receive_an_authentication_token() {
Assertions.assertThat(authToken).isNotNull();
Assertions.assertThat(authToken).isNotEmpty();
}
@Then("the response should contain message {string}")
public void the_response_should_contain_message(String expectedMessage) {
Assertions.assertThat(response.getBody()).contains(expectedMessage);
}
// DTO for login request
public static class LoginRequest {
private String username;
private String password;
public LoginRequest(String username, String password) {
this.username = username;
this.password = password;
}
// Getters and setters
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
}
}
REST API Testing
src/test/resources/features/api/user_management.feature
Feature: User Management API
As an API client
I want to manage users through REST endpoints
So that I can integrate with the user management system
Background:
Given the API is available
And I have a valid admin token
@API @UserManagement
Scenario: Create a new user via API
When I send a POST request to "/api/users" with:
"""
{
"username": "testuser",
"email": "[email protected]",
"firstName": "Test",
"lastName": "User",
"password": "SecurePass123!"
}
"""
Then the response status should be 201
And the response should contain:
"""
{
"id": 1,
"username": "testuser",
"email": "[email protected]",
"status": "ACTIVE"
}
"""
And the user should be persisted in the database
@API @UserManagement
Scenario: Get user details via API
Given a user exists with id "1"
When I send a GET request to "/api/users/1"
Then the response status should be 200
And the response should contain:
"""
{
"id": 1,
"username": "testuser",
"email": "[email protected]"
}
"""
@API @UserManagement
Scenario: Update user via API
Given a user exists with id "1"
When I send a PUT request to "/api/users/1" with:
"""
{
"firstName": "Updated",
"lastName": "Name"
}
"""
Then the response status should be 200
And the response should contain:
"""
{
"firstName": "Updated",
"lastName": "Name"
}
"""
@API @UserManagement
Scenario: Delete user via API
Given a user exists with id "1"
When I send a DELETE request to "/api/users/1"
Then the response status should be 204
And the user should be removed from the database
src/test/java/com/example/stepdefs/ApiStepDefinitions.java
package com.example.stepdefs;
import io.cucumber.java.en.Given;
import io.cucumber.java.en.When;
import io.cucumber.java.en.Then;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.*;
import org.assertj.core.api.Assertions;
import com.example.model.User;
import com.example.repository.UserRepository;
import com.fasterxml.jackson.databind.ObjectMapper;
public class ApiStepDefinitions {
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private UserRepository userRepository;
@Autowired
private ObjectMapper objectMapper;
private ResponseEntity<String> response;
private String authToken;
private HttpHeaders headers;
@Given("the API is available")
public void the_api_is_available() {
// Spring Boot test will ensure application is running
System.out.println("API is available");
}
@Given("I have a valid admin token")
public void i_have_a_valid_admin_token() {
// Obtain authentication token
authToken = "admin-token-123"; // In real implementation, get from auth service
headers = new HttpHeaders();
headers.setBearerAuth(authToken);
headers.setContentType(MediaType.APPLICATION_JSON);
}
@Given("a user exists with id {string}")
public void a_user_exists_with_id(String userId) {
User user = new User("testuser", "password", "ACTIVE");
user.setId(Long.parseLong(userId));
userRepository.save(user);
}
@When("I send a {string} request to {string} with:")
public void i_send_a_request_to_with(String method, String endpoint, String body) {
HttpMethod httpMethod = HttpMethod.valueOf(method.toUpperCase());
HttpEntity<String> request = new HttpEntity<>(body, headers);
response = restTemplate.exchange(endpoint, httpMethod, request, String.class);
}
@Then("the response status should be {int}")
public void the_response_status_should_be(Integer expectedStatus) {
Assertions.assertThat(response.getStatusCodeValue())
.isEqualTo(expectedStatus);
}
@Then("the response should contain:")
public void the_response_should_contain(String expectedJson) {
try {
// Parse both JSON strings to compare structure, ignoring extra fields
var expectedNode = objectMapper.readTree(expectedJson);
var actualNode = objectMapper.readTree(response.getBody());
// Custom assertion to check if expected fields exist in actual response
assertJsonContains(expectedNode, actualNode);
} catch (Exception e) {
throw new RuntimeException("Failed to parse JSON", e);
}
}
@Then("the user should be persisted in the database")
public void the_user_should_be_persisted_in_the_database() {
User user = userRepository.findByUsername("testuser");
Assertions.assertThat(user).isNotNull();
Assertions.assertThat(user.getEmail()).isEqualTo("[email protected]");
}
@Then("the user should be removed from the database")
public void the_user_should_be_removed_from_the_database() {
User user = userRepository.findByUsername("testuser");
Assertions.assertThat(user).isNull();
}
private void assertJsonContains(com.fasterxml.jackson.databind.JsonNode expected,
com.fasterxml.jackson.databind.JsonNode actual) {
expected.fields().forEachRemaining(entry -> {
String field = entry.getKey();
var expectedValue = entry.getValue();
var actualValue = actual.get(field);
Assertions.assertThat(actualValue)
.withFailMessage("Field '%s' not found in response", field)
.isNotNull();
Assertions.assertThat(actualValue.toString())
.withFailMessage("Field '%s' value mismatch. Expected: %s, Actual: %s",
field, expectedValue, actualValue)
.isEqualTo(expectedValue.toString());
});
}
}
Parallel Execution and Reporting
JUnit 5 Parallel Runner
src/test/java/com/example/runner/ParallelCucumberTest.java
package com.example.runner;
import org.junit.platform.suite.api.ConfigurationParameter;
import org.junit.platform.suite.api.IncludeEngines;
import org.junit.platform.suite.api.SelectClasspathResource;
import org.junit.platform.suite.api.Suite;
import static io.cucumber.junit.platform.engine.Constants.*;
@Suite
@IncludeEngines("cucumber")
@SelectClasspathResource("features")
@ConfigurationParameter(key = PLUGIN_PROPERTY_NAME,
value = "pretty, html:target/cucumber-reports/cucumber.html, json:target/cucumber-reports/cucumber.json")
@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "com.example.stepdefs")
@ConfigurationParameter(key = FILTER_TAGS_PROPERTY_NAME, value = "@Test and not @WIP")
@ConfigurationParameter(key = PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME, value = "true")
@ConfigurationParameter(key = PARALLEL_CONFIG_STRATEGY_PROPERTY_NAME, value = "fixed")
@ConfigurationParameter(key = PARALLEL_CONFIG_FIXED_PARALLELISM_PROPERTY_NAME, value = "4")
@ConfigurationParameter(key = PARALLEL_CONFIG_FIXED_MAX_POOL_SIZE_PROPERTY_NAME, value = "8")
public class ParallelCucumberTest {
}
Configuration for Parallel Execution
src/test/resources/junit-platform.properties
cucumber.execution.parallel.enabled=true cucumber.execution.parallel.config.strategy=fixed cucumber.execution.parallel.config.fixed.parallelism=4 cucumber.execution.parallel.config.fixed.max-pool-size=8 cucumber.plugin=pretty, html:target/cucumber-reports/cucumber.html, json:target/cucumber-reports/cucumber.json, junit:target/cucumber-reports/cucumber.xml cucumber.filter.tags=@Test and not @WIP
Best Practices and Patterns
1. Page Object Pattern for UI Tests
src/test/java/com/example/pages/LoginPage.java
package com.example.pages;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;
public class LoginPage {
private WebDriver driver;
@FindBy(id = "username")
private WebElement usernameField;
@FindBy(id = "password")
private WebElement passwordField;
@FindBy(id = "login-button")
private WebElement loginButton;
@FindBy(className = "error-message")
private WebElement errorMessage;
@FindBy(className = "welcome-message")
private WebElement welcomeMessage;
public LoginPage(WebDriver driver) {
this.driver = driver;
PageFactory.initElements(driver, this);
}
public void navigateTo() {
driver.get("http://localhost:8080/login");
}
public void enterUsername(String username) {
usernameField.clear();
usernameField.sendKeys(username);
}
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 welcomeMessage.getText();
}
public boolean isOnLoginPage() {
return driver.getCurrentUrl().contains("/login");
}
public boolean isOnDashboard() {
return driver.getCurrentUrl().contains("/dashboard");
}
}
2. Configuration Management
src/test/java/com/example/config/TestConfig.java
package com.example.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "test")
public class TestConfig {
private String baseUrl;
private int timeout;
private Browser browser;
public enum Browser {
CHROME, FIREFOX, EDGE
}
// Getters and setters
public String getBaseUrl() { return baseUrl; }
public void setBaseUrl(String baseUrl) { this.baseUrl = baseUrl; }
public int getTimeout() { return timeout; }
public void setTimeout(int timeout) { this.timeout = timeout; }
public Browser getBrowser() { return browser; }
public void setBrowser(Browser browser) { this.browser = browser; }
@Bean
public TestConfig testConfig() {
return this;
}
}
3. Shared Test Context
src/test/java/com/example/stepdefs/TestContext.java
package com.example.stepdefs;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import java.util.HashMap;
import java.util.Map;
@Component
public class TestContext {
private Map<String, Object> context = new HashMap<>();
private RestTemplate restTemplate = new RestTemplate();
public void set(String key, Object value) {
context.put(key, value);
}
@SuppressWarnings("unchecked")
public <T> T get(String key) {
return (T) context.get(key);
}
public <T> T get(String key, Class<T> type) {
return type.cast(context.get(key));
}
public void clear() {
context.clear();
}
public RestTemplate getRestTemplate() {
return restTemplate;
}
}
Running Tests
Command Line Execution
# Run all tests mvn test # Run specific feature mvn test -Dcucumber.options="src/test/resources/features/login" # Run with specific tags mvn test -Dcucumber.options="--tags @Login" # Run with specific tags excluding others mvn test -Dcucumber.options="--tags @Test --tags ~@WIP" # Generate reports mvn test -Dcucumber.options="--plugin html:target/cucumber-reports"
Common Cucumber Options
@CucumberOptions(
features = "src/test/resources/features",
glue = "com.example.stepdefs",
plugin = {
"pretty", // Console output
"html:target/cucumber-reports/cucumber.html", // HTML report
"json:target/cucumber-reports/cucumber.json", // JSON report
"junit:target/cucumber-reports/cucumber.xml", // JUnit report
"rerun:target/rerun.txt" // Rerun failed tests
},
monochrome = true, // Clean console output
tags = "@Regression and not @Slow", // Tag filtering
dryRun = false, // Check step definitions without running
strict = true, // Fail if undefined steps
snippets = SnippetType.CAMELCASE // Step definition naming
)
This comprehensive BDD testing setup with Cucumber in Java provides a robust framework for writing executable specifications that bridge the gap between business requirements and technical implementation.