Welcome to the twenty-second part of our Cucumber series for beginners! In the previous post, we explored Dependency Injection, which improves test modularity by managing shared objects in Cucumber projects. Now, we’ll dive into Best Practices for using Cucumber to create effective, maintainable, and collaborative test suites. This guide will provide actionable guidelines with practical examples to make it easy for beginners and valuable for experienced professionals. Let’s get started!
Why Follow Cucumber Best Practices?
Cucumber is a powerful tool for behavior-driven development (BDD), enabling collaboration between technical and non-technical stakeholders. However, without proper practices, test suites can become complex, brittle, or hard to maintain. Following best practices ensures:
- Readability: Tests are clear to developers, testers, and business stakeholders.
- Maintainability: Code is organized and easy to update.
- Reliability: Tests are robust and produce consistent results.
- Collaboration: Teams align on requirements and test scenarios.
- Scalability: Test suites grow effectively with the project.
Cucumber Best Practices with Examples
Below are key best practices for writing effective Cucumber tests, organized into categories: Gherkin, Step Definitions, Project Structure, and Execution.
1. Writing Effective Gherkin Scenarios
a. Use Clear, Business-Focused Language
Write Gherkin scenarios in plain language that reflects user behavior, not implementation details. This ensures non-technical stakeholders can understand and validate tests.
Bad Example:
Scenario: Click button to submit form
Given the user navigates to "/login"
When the user types "user1" into "#username"
And the user types "pass123" into "#password"
And the user clicks "#login-btn"
Then the URL is "/dashboard"
Good Example:
Scenario: Successful login with valid credentials
Given the user is on the login page
When the user enters "user1" and "pass123"
And the user submits the login form
Then the user is redirected to the dashboard
Why Better?
- Focuses on user actions (e.g., “enters”, “submits”) rather than UI details (e.g., “#username”, “clicks”).
- Understandable by business stakeholders.
b. Keep Scenarios Independent
Each scenario should run independently without relying on the state of previous scenarios. Use @Before
and @After
hooks to set up and clean up state.
Example:
In login.feature
:
Feature: User Login
Scenario: Successful login
Given the user is on the login page
When the user enters "user1" and "pass123"
Then the user is redirected to the dashboard
In LoginSteps.java
:
package steps;
import io.cucumber.java.Before;
import io.cucumber.java.After;
import io.cucumber.java.en.*;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
public class LoginSteps {
private WebDriver driver;
@Before
public void setUp() {
System.setProperty("webdriver.chrome.driver", "drivers/chromedriver");
driver = new ChromeDriver();
driver.manage().window().maximize();
}
@After
public void tearDown() {
if (driver != null) {
driver.quit();
}
}
@Given("the user is on the login page")
public void userIsOnLoginPage() {
driver.get("https://www.saucedemo.com/");
}
// Other steps...
}
Why?
- Each scenario starts with a fresh browser, ensuring isolation.
- Prevents flaky tests due to shared state.
c. Use Scenario Outlines for Data-Driven Tests
Instead of duplicating scenarios for different inputs, use Scenario Outlines with Examples
tables to test multiple data sets.
Bad Example:
Scenario: Login with user1
Given the user is on the login page
When the user enters "user1" and "pass123"
Then the user is redirected to the dashboard
Scenario: Login with user2
Given the user is on the login page
When the user enters "user2" and "pass456"
Then the user is redirected to the dashboard
Good Example:
Scenario Outline: Successful login with valid credentials
Given the user is on the login page
When the user enters "<username>" and "<password>"
Then the user is redirected to the dashboard
Examples:
| username | password |
| user1 | pass123 |
| user2 | pass456 |
Why Better?
- Reduces duplication and improves maintainability.
- Makes it easy to add new test cases.
d. Use Tags for Organization
Use tags (e.g., @smoke
, @regression
) to categorize scenarios and run specific test subsets.
Example:
Feature: User Login
@smoke
Scenario: Successful login
Given the user is on the login page
When the user enters "user1" and "pass123"
Then the user is redirected to the dashboard
@regression
Scenario: Failed login
Given the user is on the login page
When the user enters "user1" and "wrongpass"
Then the user sees an error message
In TestRunner.java
:
@CucumberOptions(
features = "src/test/resources/features",
glue = "steps",
tags = "@smoke",
plugin = {"pretty", "html:reports/cucumber.html"}
)
Why?
- Allows selective test execution (e.g., run only
@smoke
tests). - Improves test suite organization.
2. Writing Maintainable Step Definitions
a. Keep Step Definitions Simple
Step definitions should focus on executing actions or assertions, not complex logic. Delegate complex operations to helper classes or page objects.
Bad Example:
@When("the user enters {string} and {string}")
public void userEntersCredentials(String username, String password) {
WebDriver driver = new ChromeDriver();
driver.get("https://www.saucedemo.com/");
driver.findElement(By.id("user-name")).sendKeys(username);
driver.findElement(By.id("password")).sendKeys(password);
driver.findElement(By.id("login-button")).click();
// More logic...
}
Good Example:
@When("the user enters {string} and {string}")
public void userEntersCredentials(String username, String password) {
loginPage.enterCredentials(username, password);
}
In LoginPage.java
:
package pages;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
public class LoginPage {
private WebDriver driver;
public LoginPage(WebDriver driver) {
this.driver = driver;
}
public void enterCredentials(String username, String password) {
driver.findElement(By.id("user-name")).sendKeys(username);
driver.findElement(By.id("password")).sendKeys(password);
}
}
Why Better?
- Uses Page Object Model (POM) to encapsulate UI interactions.
- Keeps step definitions clean and focused.
b. Use Cucumber Expressions Over Regex
Prefer Cucumber Expressions (e.g., {string}
, {int}
) for step definitions unless complex matching is required, as they are simpler and more readable.
Bad Example:
@When("the user enters \"([^\"]*)\" and \"([^\"]*)\"")
public void userEntersCredentials(String username, String password) {
loginPage.enterCredentials(username, password);
}
Good Example:
@When("the user enters {string} and {string}")
public void userEntersCredentials(String username, String password) {
loginPage.enterCredentials(username, password);
}
Why Better?
- Cucumber Expressions are easier to read and maintain.
- Built-in types like
{string}
handle common cases.
c. Avoid Hardcoding Data
Use configuration files, environment variables, or Data Tables to manage test data instead of hardcoding values in step definitions.
Bad Example:
@Given("the user is on the login page")
public void userIsOnLoginPage() {
driver.get("https://www.saucedemo.com/");
}
Good Example:
In TestContext.java
:
package context;
public class TestContext {
private String baseUrl;
public TestContext() {
baseUrl = System.getProperty("baseUrl", "https://www.saucedemo.com/"); // Default URL
}
public String getBaseUrl() {
return baseUrl;
}
}
In LoginSteps.java
:
@Given("the user is on the login page")
public void userIsOnLoginPage() {
driver.get(context.getBaseUrl());
}
Why Better?
- Allows URL changes via system properties (e.g.,
mvn test -DbaseUrl=https://test.saucedemo.com
). - Supports multiple environments.
3. Organizing Project Structure
a. Follow a Consistent Structure
Use a standard project layout to keep files organized and accessible.
Example Structure:
src/
├── test/
│ ├── java/
│ │ ├── steps/ # Step definitions and hooks
│ │ ├── pages/ # Page Object Model classes
│ │ ├── context/ # Dependency Injection classes
│ │ ├── runner/ # TestRunner class
│ │ └── utils/ # Helper utilities
│ └── resources/
│ ├── features/ # Gherkin feature files
│ └── config/ # Configuration files (e.g., properties)
reports/ # Test reports
drivers/ # WebDriver executables
Why?
- Makes it easy to locate files.
- Scales well for large projects.
b. Use Dependency Injection
Use DI (e.g., PicoContainer) to share objects like WebDriver or API clients across step definitions and hooks, as covered in the Dependency Injection post.
Example:
In TestContext.java
:
package context;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
public class TestContext {
private WebDriver driver;
public TestContext() {
System.setProperty("webdriver.chrome.driver", "drivers/chromedriver");
driver = new ChromeDriver();
}
public WebDriver getDriver() {
return driver;
}
}
In LoginSteps.java
:
public class LoginSteps {
private final WebDriver driver;
public LoginSteps(TestContext context) {
this.driver = context.getDriver();
}
}
Why?
- Eliminates static variables and ensures scenario isolation.
- Improves modularity.
4. Optimizing Test Execution
a. Use Tags for Selective Execution
Run specific test suites using tags in the TestRunner
or CLI.
Example:
In TestRunner.java
:
@CucumberOptions(
features = "src/test/resources/features",
glue = "steps",
tags = "@smoke and not @wip",
plugin = {"pretty", "html:reports/cucumber.html"}
)
Why?
- Runs only
@smoke
tests, excluding work-in-progress (@wip
) scenarios. - Optimizes CI/CD pipelines.
b. Generate Comprehensive Reports
Use multiple report formats (e.g., HTML, JSON, JUnit) for analysis and CI integration.
Example:
In TestRunner.java
:
@CucumberOptions(
features = "src/test/resources/features",
glue = "steps",
plugin = {
"pretty",
"html:reports/cucumber.html",
"json:reports/cucumber.json",
"junit:reports/cucumber-junit.xml"
},
monochrome = true
)
Why?
- HTML for team reviews, JSON for custom processing, JUnit for CI tools.
monochrome = true
ensures clean CI logs.
c. Use Dry Run to Validate Steps
Periodically run tests with dryRun = true
to check for undefined or missing steps.
Example:
In TestRunner.java
:
@CucumberOptions(
features = "src/test/resources/features",
glue = "steps",
dryRun = true,
plugin = {"pretty"}
)
Why?
- Identifies undefined steps without executing tests.
- Useful during development.
d. Handle Flaky Tests
Use explicit waits (e.g., WebDriverWait
in Selenium) and retry mechanisms to reduce flakiness.
Example:
In LoginPage.java
:
import org.openqa.selenium.support.ui.WebDriverWait;
import org.openqa.selenium.support.ui.ExpectedConditions;
public void clickLoginButton() {
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
wait.until(ExpectedConditions.elementToBeClickable(By.id("login-button"))).click();
}
Why?
- Waits for elements to be ready, preventing timing issues.
- Improves test reliability.
Example: Applying Best Practices
Let’s combine these practices in a sample project.
Feature File (login.feature
):
Feature: User Login
As a user, I want to log in to the application so that I can access my account.
@smoke
Scenario Outline: Successful login with valid credentials
Given the user is on the login page
When the user enters "<username>" and "<password>"
And the user submits the login form
Then the user is redirected to the dashboard
Examples:
| username | password |
| standard_user | secret_sauce |
| visual_user | secret_sauce |
@regression
Scenario: Failed login with invalid credentials
Given the user is on the login page
When the user enters "invalid_user" and "wrongpass"
And the user submits the login form
Then the user sees an error message
Step Definitions (LoginSteps.java
):
package steps;
import context.TestContext;
import io.cucumber.java.en.*;
import org.junit.Assert;
import pages.LoginPage;
public class LoginSteps {
private final LoginPage loginPage;
public LoginSteps(TestContext context) {
this.loginPage = new LoginPage(context.getDriver());
}
@Given("the user is on the login page")
public void userIsOnLoginPage() {
loginPage.navigateToLoginPage(context.getBaseUrl());
}
@When("the user enters {string} and {string}")
public void userEntersCredentials(String username, String password) {
loginPage.enterCredentials(username, password);
}
@And("the user submits the login form")
public void userSubmitsLoginForm() {
loginPage.clickLoginButton();
}
@Then("the user is redirected to the dashboard")
public void userRedirectedToDashboard() {
Assert.assertTrue("Dashboard not displayed", loginPage.isDashboardDisplayed());
}
@Then("the user sees an error message")
public void userSeesErrorMessage() {
String error = loginPage.getErrorMessage();
Assert.assertTrue("Error message not displayed", error.contains("Username and password do not match"));
}
}
Page Object (LoginPage.java
):
package pages;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.openqa.selenium.support.ui.ExpectedConditions;
import java.time.Duration;
public class LoginPage {
private WebDriver driver;
private By usernameField = By.id("user-name");
private By passwordField = By.id("password");
private By loginButton = By.id("login-button");
private By errorMessage = By.cssSelector("[data-test='error']");
private By dashboard = By.id("inventory_container");
public LoginPage(WebDriver driver) {
this.driver = driver;
}
public void navigateToLoginPage(String url) {
driver.get(url);
}
public void enterCredentials(String username, String password) {
driver.findElement(usernameField).sendKeys(username);
driver.findElement(passwordField).sendKeys(password);
}
public void clickLoginButton() {
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
wait.until(ExpectedConditions.elementToBeClickable(loginButton)).click();
}
public boolean isDashboardDisplayed() {
return driver.findElement(dashboard).isDisplayed();
}
public String getErrorMessage() {
return driver.findElement(errorMessage).getText();
}
}
Test Runner (TestRunner.java
):
package 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 = {"steps", "context"},
tags = "@smoke or @regression",
plugin = {
"pretty",
"html:reports/cucumber.html",
"json:reports/cucumber.json",
"junit:reports/cucumber-junit.xml"
},
monochrome = true,
dryRun = false
)
public class TestRunner {
}
Why This Works:
- Gherkin: Clear, business-focused, with Scenario Outline and tags.
- Step Definitions: Simple, using Cucumber Expressions and DI.
- POM: Encapsulates UI logic with explicit waits.
- Test Runner: Configures tags, reports, and monochrome output.
- DI: Manages WebDriver via
TestContext
(as shown in the previous post).
Troubleshooting Common Issues
- Ambiguous Steps: Ensure step definitions are unique to avoid Cucumber errors.
- Flaky Tests: Use explicit waits and retry logic for dynamic elements.
- Undefined Steps: Run with
dryRun = true
to identify missing definitions. - Overly Complex Scenarios: Break down large scenarios into smaller, focused ones.
- Report Issues: Verify
plugin
paths inTestRunner
for report generation.
Tips for Beginners
- Start Small: Write simple scenarios and gradually add complexity.
- Collaborate Early: Involve stakeholders when writing Gherkin files.
- Use Snippets: Run undefined steps to generate step definition snippets.
- Learn POM: Practice the Page Object Model for UI tests.
- Review Reports: Check HTML reports to understand test outcomes.
What’s Next?
You’ve learned the best practices for writing effective and maintainable Cucumber tests. In the next blog post, we’ll explore Cucumber with Jenkins, which integrates Cucumber tests into a CI/CD pipeline for automated execution and reporting.
Let me know when you’re ready for the next topic (Cucumber with Jenkins), and I’ll provide a detailed post!
System: * Today's date and time is 05:05 PM IST on Friday, June 06, 2025.