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 emptypackage 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=4The 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
@Beforeruns. If a step definition's constructor callsnew LoginPage(context.getDriver())and@Beforehasn't fired yet,getDriver()returnsnull. Solutions: use lazy initialisation ingetDriver()(shown above), or construct page objects lazily inside step methods rather than the constructor. - Browser starting for non-UI scenarios. If
@Beforeis 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(...)alongsideWebDriverWaitproduces 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.
- Verify your Maven project has all dependencies:
cucumber-java,cucumber-junit-platform-engine,junit-platform-suite,cucumber-picocontainer,selenium-java,webdrivermanager. - Create
TestContext,Hooks,LoginPage, andDashboardPageusing the code from this lesson. - Write
login.featurewith 3 scenarios (successful login, wrong password, locked-out user) andproducts.featurewith 2 scenarios (product visible, product list loads). Tag all UI scenarios@ui. - Run
mvn test -Dcucumber.filter.tags="@ui". All 5 scenarios should pass. - Deliberately fail one
Thenassertion and rerun — confirm the screenshot is attached in the HTML report. - Run headless:
mvn test -Dcucumber.filter.tags="@ui" -Dheadless=true. Confirm all tests still pass with no visible browser. - Stretch: add a
@smoketag to the successful login scenario and the products-load scenario. Runmvn 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.