Cucumber with Selenium WebDriver

9 min read

Everything from the previous three chapters comes together here. Feature files describe behaviour in Gherkin. Step definitions delegate to page objects. PicoContainer shares the WebDriver across classes. Hooks manage the browser lifecycle and attach screenshots on failure. This lesson shows the full wiring — a production-ready Cucumber + Selenium project from Maven dependencies to green scenarios.

The TestContext with lazy driver initialisation

A lazy-initialised driver avoids creating a browser for scenarios that don't need it (API scenarios, unit-level scenarios). The @Before("@ui") hook triggers the first getDriver() call:

package context;
 
import io.github.bonigarcia.wdm.WebDriverManager;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
 
public class TestContext {
    private WebDriver driver;
    private String authToken;
 
    public WebDriver getDriver() {
        if (driver == null) {
            ChromeOptions options = new ChromeOptions();
            if (Boolean.parseBoolean(System.getProperty("headless", "false"))) {
                options.addArguments("--headless=new");
            }
            WebDriverManager.chromedriver().setup();
            driver = new ChromeDriver(options);
            driver.manage().window().maximize();
        }
        return driver;
    }
 
    public void setAuthToken(String token) { this.authToken = token; }
    public String getAuthToken() { return authToken; }
 
    public void quitDriver() {
        if (driver != null) {
            driver.quit();
            driver = null;
        }
    }
}

System.getProperty("headless", "false") reads the Maven property -Dheadless=true. CI pipelines set this; local development leaves it unset.

Hooks with screenshot capture

package hooks;
 
import context.TestContext;
import io.cucumber.java.After;
import io.cucumber.java.Before;
import io.cucumber.java.Scenario;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.TakesScreenshot;
 
public class Hooks {
    private final TestContext context;
 
    public Hooks(TestContext context) {
        this.context = context;
    }
 
    @Before("@ui")
    public void setupBrowser() {
        context.getDriver();  // triggers lazy init
    }
 
    @After
    public void afterScenario(Scenario scenario) {
        if (scenario.isFailed() && context.getDriver() != null) {
            byte[] screenshot = ((TakesScreenshot) context.getDriver())
                .getScreenshotAs(OutputType.BYTES);
            scenario.attach(screenshot, "image/png", scenario.getName());
        }
        context.quitDriver();
    }
}

The @After hook runs for every scenario — whether it's tagged @ui or not. The null check prevents errors on non-UI scenarios where the driver was never started.

A complete feature and step definition

@ui
Feature: Product Search
 
  Background:
    Given the user is logged into Sauce Demo
 
  @smoke
  Scenario: Search returns matching products
    When the user filters products by "Sauce Labs Backpack"
    Then the product list should contain "Sauce Labs Backpack"
 
  Scenario: No results shows empty state
    When the user filters products by "XYZNOTAPRODUCT"
    Then the product list should be empty
package stepdefinitions;
 
import context.TestContext;
import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;
import pages.DashboardPage;
import pages.LoginPage;
 
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
 
public class ProductSteps {
    private final TestContext context;
    private final LoginPage loginPage;
    private final DashboardPage dashboardPage;
 
    public ProductSteps(TestContext context) {
        this.context = context;
        this.loginPage = new LoginPage(context.getDriver());
        this.dashboardPage = new DashboardPage(context.getDriver());
    }
 
    @Given("the user is logged into Sauce Demo")
    public void loggedIn() {
        loginPage.navigateTo();
        loginPage.loginAs("standard_user", "secret_sauce");
    }
 
    @When("the user filters products by {string}")
    public void filterProducts(String productName) {
        dashboardPage.filterByName(productName);
    }
 
    @Then("the product list should contain {string}")
    public void verifyProductPresent(String productName) {
        List<String> products = dashboardPage.getProductNames();
        assertTrue(products.contains(productName),
            "Expected product '" + productName + "' but found: " + products);
    }
 
    @Then("the product list should be empty")
    public void verifyNoProducts() {
        assertTrue(dashboardPage.getProductNames().isEmpty(),
            "Expected no products but the list was not empty");
    }
}

Running from Maven

# All UI scenarios
mvn test -Dcucumber.filter.tags="@ui"
 
# Smoke suite, headless
mvn test -Dcucumber.filter.tags="@smoke" -Dheadless=true
 
# Full regression, 4 threads, headless (Chapter 4 Lesson 3)
mvn test -Dcucumber.filter.tags="@regression" -Dheadless=true \
  -Dcucumber.execution.parallel.enabled=true \
  -Dcucumber.execution.parallel.config.fixed.parallelism=4

The full execution flow

Step 1 of 7

Gherkin parsed

Cucumber reads login.feature, identifies the @ui tag and Background steps. Builds an execution plan: Background + 2 scenarios.

Handling waits in a BDD context

Explicit waits belong in page objects, not step definitions and not hooks:

// In DashboardPage — not in the step definition
public List<String> getProductNames() {
    WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
    wait.until(ExpectedConditions.visibilityOfElementLocated(
        By.cssSelector(".inventory_item_name")));
    return driver.findElements(By.cssSelector(".inventory_item_name"))
        .stream()
        .map(WebElement::getText)
        .collect(Collectors.toList());
}

Waits in step definitions leak timing concerns into the Gherkin-translation layer. Waits in page objects mean changing a wait affects exactly one method in one class — not a hunt through step definition files. The Selenium with Java waits chapter covers explicit waits in depth.

⚠️ Common mistakes

  • Constructing page objects before @Before runs. If a step definition's constructor calls new LoginPage(context.getDriver()) and @Before hasn't fired yet, getDriver() returns null. Solutions: use lazy initialisation in getDriver() (shown above), or construct page objects lazily inside step methods rather than the constructor.
  • Browser starting for non-UI scenarios. If @Before is not tagged @ui, a browser starts for every scenario including API tests. Always scope the browser hook with @Before("@ui").
  • Implicit waits mixed with explicit waits. Setting driver.manage().timeouts().implicitlyWait(...) alongside WebDriverWait produces unpredictable timing behaviour. Pick explicit waits throughout the page objects and never set implicit waits.

🎯 Practice task

Build and run a complete Sauce Demo BDD test suite with Cucumber + Selenium + POM. 50–60 minutes.

  1. Verify your Maven project has all dependencies: cucumber-java, cucumber-junit-platform-engine, junit-platform-suite, cucumber-picocontainer, selenium-java, webdrivermanager.
  2. Create TestContext, Hooks, LoginPage, and DashboardPage using the code from this lesson.
  3. Write login.feature with 3 scenarios (successful login, wrong password, locked-out user) and products.feature with 2 scenarios (product visible, product list loads). Tag all UI scenarios @ui.
  4. Run mvn test -Dcucumber.filter.tags="@ui". All 5 scenarios should pass.
  5. Deliberately fail one Then assertion and rerun — confirm the screenshot is attached in the HTML report.
  6. Run headless: mvn test -Dcucumber.filter.tags="@ui" -Dheadless=true. Confirm all tests still pass with no visible browser.
  7. Stretch: add a @smoke tag to the successful login scenario and the products-load scenario. Run mvn test -Dcucumber.filter.tags="@smoke and @ui" -Dheadless=true — only those 2 scenarios should execute.

Next lesson: BDD for APIs — Cucumber scenarios driving Rest Assured.

// tip to track lessons you complete and pick up where you left off across devices.