A login scenario has Given steps in LoginSteps.java and Then steps in DashboardSteps.java. Both need the same WebDriver instance. A checkout scenario stores a generated order ID in one step and asserts on it in a later step. Step definitions can't reach into each other — so how does state flow between them?
This lesson answers that question properly. Static fields are the wrong answer. Dependency injection is the right one.
Why you can't use instance fields across classes
Cucumber creates a new instance of every step definition class for each scenario. That's by design — it guarantees scenario isolation. But it also means you can't write:
public class SharedState {
public static WebDriver driver; // ❌ static — persists across scenarios
}A static field survives the scenario boundary. Scenario 2 picks up the browser session that scenario 1 quit, gets a NoSuchSessionException, and you spend an hour debugging a problem that's architectural, not code. Static is a shortcut that breaks isolation and parallelism at the same time.
Option 1: instance variables (one-class scenarios)
When all steps for a scenario live in the same class, instance fields are fine:
public class LoginSteps {
private WebDriver driver;
private String authToken;
@Before
public void setup() {
driver = new ChromeDriver();
}
@Given("the user is on the login page")
public void onLoginPage() {
driver.get(System.getProperty("baseUrl") + "/login");
}
@When("the user logs in with {string} and {string}")
public void login(String email, String password) {
driver.findElement(By.id("email")).sendKeys(email);
driver.findElement(By.id("password")).sendKeys(password);
driver.findElement(By.id("submit")).click();
}
@Then("the user should be on the dashboard")
public void verifyDashboard() {
assertTrue(driver.getCurrentUrl().contains("/dashboard"));
}
@After
public void teardown() {
if (driver != null) driver.quit();
}
}This works cleanly — but only while all related steps stay in one class. As soon as a second class needs the driver, instance fields stop working.
Option 2: PicoContainer dependency injection (recommended)
PicoContainer is a lightweight DI container bundled with cucumber-picocontainer. Add the dependency:
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-picocontainer</artifactId>
<version>7.18.0</version>
<scope>test</scope>
</dependency>No configuration needed — just add the artifact. Cucumber detects it automatically.
Step 1 — create a shared context class:
package context;
import org.openqa.selenium.WebDriver;
import java.util.HashMap;
import java.util.Map;
public class TestContext {
private WebDriver driver;
private String authToken;
private final Map<String, Object> scenarioData = new HashMap<>();
public WebDriver getDriver() { return driver; }
public void setDriver(WebDriver driver) { this.driver = driver; }
public String getAuthToken() { return authToken; }
public void setAuthToken(String token) { this.authToken = token; }
public void put(String key, Object value) { scenarioData.put(key, value); }
public Object get(String key) { return scenarioData.get(key); }
public void quitDriver() {
if (driver != null) {
driver.quit();
driver = null;
}
}
}Step 2 — inject it via constructors:
public class LoginSteps {
private final TestContext context;
public LoginSteps(TestContext context) {
this.context = context;
}
@Given("the user is on the login page")
public void onLoginPage() {
context.getDriver().get(System.getProperty("baseUrl") + "/login");
}
@When("the user logs in with {string} and {string}")
public void login(String email, String password) {
WebDriver driver = context.getDriver();
driver.findElement(By.id("email")).sendKeys(email);
driver.findElement(By.id("password")).sendKeys(password);
driver.findElement(By.id("submit")).click();
}
}
public class DashboardSteps {
private final TestContext context;
public DashboardSteps(TestContext context) {
this.context = context;
}
@Then("the user should be on the dashboard")
public void verifyDashboard() {
assertTrue(context.getDriver().getCurrentUrl().contains("/dashboard"));
}
}
public class Hooks {
private final TestContext context;
public Hooks(TestContext context) {
this.context = context;
}
@Before("@ui")
public void setupBrowser() {
WebDriverManager.chromedriver().setup();
context.setDriver(new ChromeDriver());
}
@After
public void teardown(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();
}
}PicoContainer sees that LoginSteps, DashboardSteps, and Hooks all declare TestContext as a constructor parameter. It creates one TestContext instance per scenario and injects the same object into all three classes. State flows between classes through that shared object. At scenario end, the instance is discarded — the next scenario gets a fresh one.
Storing scenario data between steps
The scenarioData map in TestContext is a catch-all for values that steps need to pass forward:
@When("the user creates an order for {string}")
public void createOrder(String product) {
String orderId = orderService.create(product);
context.put("orderId", orderId); // store for later steps
}
@Then("the order confirmation page should show the order ID")
public void verifyOrderId() {
String expectedId = (String) context.get("orderId");
assertTrue(orderPage.getConfirmationText().contains(expectedId));
}The map trades type safety for flexibility. For frequently-used values (driver, auth token), use typed fields. For step-to-step transient data (generated IDs, extracted values), the map works well.
Why PicoContainer, not Spring or Guice?
Cucumber supports multiple DI containers: PicoContainer, Spring, Guice, Weld, and others. PicoContainer is the default recommendation for test projects because:
- Zero configuration — no XML, no annotations beyond the constructor parameter declaration
- No classpath scanning or context initialisation overhead
- Ships as
cucumber-picocontainer— no separate Spring Boot or Guice setup - Purpose-built for test scenarios: create, inject, discard
Use Spring injection if your project already uses Spring (e.g., you want to autowire production beans into step definitions for integration testing). For standalone BDD framework projects, PicoContainer is the lighter choice.
The DI flow at a glance
- – LoginSteps
- – DashboardSteps
- – CheckoutSteps
- – Hooks
- – WebDriver
- – Auth token
- – Scenario data map
- – New instance per scenario
- – Discarded after @After
- – No leakage between scenarios
- PicoContainer (automatic) –
- No configuration needed –
⚠️ Common mistakes
- Static fields as an "easy" shortcut. Static
WebDriverfields break isolation and causeNoSuchSessionExceptionin parallel runs. The correct fix — PicoContainer — takes 5 minutes to set up once and never breaks again. TestContextwith a no-arg constructor callednew TestContext()inside step definitions. Each class callingnew TestContext()creates its own instance — no sharing happens. Always inject via the constructor; never instantiate inside the step definition class.- Forgetting
cucumber-picocontainerinpom.xml. Without the artifact, Cucumber won't inject constructor parameters — it will try to call a no-arg constructor and throwNoSuchMethodExceptionif none exists. The error message is unhelpful; check the dependency first. - Mutable shared state without cleanup. If
TestContext.scenarioDataholds a value from scenario 1 and you rely on it in scenario 2, the test is order-dependent. Always clear transient data in@Afteror use a fresh map per scenario (PicoContainer's fresh-instance-per-scenario guarantees this ifscenarioDatais an instance field, not a static one).
🎯 Practice task
Wire a multi-class step definition project with PicoContainer. 40–50 minutes.
- Add
cucumber-picocontainerto yourpom.xml. Createcontext/TestContext.javawith fields forWebDriver,authToken, and aMap<String, Object>for ad-hoc data. - Refactor your
LoginSteps.javato receiveTestContextvia constructor injection. Move driver creation toHooks.javawith@Before("@ui"). - Create
DashboardSteps.javawith a@Thenstep that verifies the dashboard URL. It should receive the sameTestContextvia constructor and read the driver from it. - Run a login scenario that involves steps in both
LoginStepsandDashboardSteps. Confirm both classes share the same driver (noNullPointerException, dashboard assertion passes). - Add a
@Whenstep that stores a value incontext.put("key", value)and a@Thenstep (in a different step definition class) that retrieves it withcontext.get("key"). Confirm the value flows across the class boundary. - Stretch: run two scenarios back to back and add a log line in
@Beforethat prints theTestContextobject hash code. Confirm a different hash code appears for each scenario — proving PicoContainer creates a fresh instance per scenario.
Next lesson: combining Cucumber with the Page Object Model for a clean, layered BDD architecture.