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

Next Steps