Cucumber Best Practices

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:


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?

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?

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?

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?

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?

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?

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?

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?

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?

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?

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?

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?

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?


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:


Troubleshooting Common Issues


Tips for Beginners

🤖
PrepCampusPlus AI Tutor
Scroll to Top