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