Zero-config Cucumber + Selenium in Java: no @Before, no driver setup, no cleanup

java dev.to

The Cucumber + Selenium boilerplate problem

Every Cucumber + Selenium project in Java starts the same way. Before you write a single step, you write the infrastructure:

public class Hooks {

    private WebDriver driver;

    @Before
    public void setUp() {
        WebDriverManager.chromedriver().setup();
        ChromeOptions options = new ChromeOptions();
        driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));
        driver = new ChromeDriver(options);
        DriverHolder.set(driver);
    }

    @After
    public void tearDown(Scenario scenario) {
        if (scenario.isFailed()) {
            byte[] screenshot = ((TakesScreenshot) driver).getScreenshotAs(OutputType.BYTES);
            scenario.attach(screenshot, "image/png", "Failure screenshot");
        }
        if (driver != null) driver.quit();
        DriverHolder.remove();
    }
}
Enter fullscreen mode Exit fullscreen mode

Then you wire a ThreadLocal for parallel safety. Then you figure out how to get it into step definitions. Then you add a static helper class. Then someone wants screenshots embedded in the Selenium Boot HTML report too, so you add that. Then CI needs headless mode...

By the time the first feature file runs, you've written 200 lines of plumbing that has nothing to do with your tests.


What the step definitions should look like

This is what a step definition file should be:

public class LoginSteps extends BaseCucumberSteps {

    @Given("the user is on the login page")
    public void onLoginPage() {
        open();
    }

    @When("they login as {string} with password {string}")
    public void login(String username, String password) {
        new LoginPage(getDriver()).login(username, password);
    }

    @Then("the dashboard should be visible")
    public void dashboardVisible() {
        assertThat(By.id("dashboard")).isVisible();
    }
}
Enter fullscreen mode Exit fullscreen mode

Pure Gherkin logic. No driver setup. No cleanup. No thread-locals. No screenshot code. The infrastructure should live somewhere else — ideally somewhere you never have to touch.


How to get there

The right place for the infrastructure is a framework-level @Before/@After hook that runs automatically for every project. Cucumber discovers hooks by scanning the glue packages — so if you put your hooks in a published library and include that package in glue, they run without any code in the consumer project.

Here's the structure:

selenium-boot (library)
└── com.seleniumboot.cucumber
    ├── CucumberHooks.java       ← @Before + @After per scenario
    ├── BaseCucumberSteps.java   ← getDriver(), open(), assertThat()
    ├── BaseCucumberTest.java    ← runner base class
    └── CucumberStepLogger.java  ← pipes step names into HTML report

your-test-project
└── com.yourcompany.bdd
    ├── CucumberRunner.java      ← @CucumberOptions + extends BaseCucumberTest
    └── steps/
        └── LoginSteps.java      ← extends BaseCucumberSteps, pure steps only
Enter fullscreen mode Exit fullscreen mode

The framework hook

CucumberHooks is the core of the integration. It runs before and after every scenario — the consumer project never sees it:

public class CucumberHooks {

    @Before(order = 1000)
    public void beforeScenario(Scenario scenario) {
        FrameworkBootstrap.initialize();        // idempotent — safe to call per-scenario
        String testId = buildTestId(scenario);

        CucumberContext.setScenario(scenario);
        SeleniumBootContext.setCurrentTestId(testId);
        ExecutionMetrics.markStart(testId);

        DriverManager.createDriver();           // reads selenium-boot.yml, applies config
        HookRegistry.onTestStart(testId);
    }

    @After(order = 20000)
    public void afterScenario(Scenario scenario) {
        String testId = SeleniumBootContext.getCurrentTestId();

        if (scenario.isFailed()) {
            // Screenshot into Selenium Boot HTML report
            String path = ScreenshotManager.capture(sanitize(scenario.getName()));
            ExecutionMetrics.recordScreenshot(testId, path);

            // Screenshot bytes attached to Cucumber's own HTML report
            String base64 = ScreenshotManager.captureAsBase64();
            if (base64 != null) {
                scenario.attach(Base64.getDecoder().decode(base64), "image/png", "Failure");
            }
            HookRegistry.onTestFailure(testId, new RuntimeException(scenario.getName()));
        }

        ExecutionMetrics.recordStatus(testId, resolveStatus(scenario));
        ExecutionMetrics.markEnd(testId);

        DriverManager.quitDriver();
        CucumberContext.clear();
        SeleniumBootContext.clearCurrentTestId();
    }
}
Enter fullscreen mode Exit fullscreen mode

The @After(order = 20000) runs before any user-defined @After(order = 10000) — so the screenshot is captured while the page is still loaded, before user cleanup hooks navigate away.


The step definition base class

BaseCucumberSteps gives step definitions access to the driver and the framework API without static helpers or thread-local lookups:

public abstract class BaseCucumberSteps {

    protected WebDriver getDriver() {
        return DriverManager.getDriver();       // ThreadLocal — safe for parallel
    }

    protected void open() {
        getDriver().get(SeleniumBootContext.getConfig().getExecution().getBaseUrl());
    }

    protected void open(String path) {
        String base = SeleniumBootContext.getConfig().getExecution().getBaseUrl();
        getDriver().get(base + path);
    }

    protected Locator $(String css)     { return Locator.ofCss(css); }
    protected Locator $(By by)          { return Locator.of(by); }

    protected LocatorAssert assertThat(By locator) {
        return SeleniumAssert.assertThat(locator);   // auto-retrying assertion
    }

    protected Scenario getScenario() {
        return CucumberContext.getScenario();
    }
}
Enter fullscreen mode Exit fullscreen mode

The runner

The runner is the one piece that belongs in the consumer project — it's how you specify where your features and step packages are:

@CucumberOptions(
    features = "src/test/resources/features",
    glue     = {"com.yourcompany.bdd.steps", "com.seleniumboot.cucumber"},
    plugin   = {"pretty", "com.seleniumboot.cucumber.CucumberStepLogger"}
)
public class CucumberRunner extends BaseCucumberTest {}
Enter fullscreen mode Exit fullscreen mode

com.seleniumboot.cucumber in glue is the key — it makes Cucumber discover CucumberHooks automatically.

CucumberStepLogger is a ConcurrentEventListener that intercepts TestStepFinished events and pipes step names and their pass/fail status into the Selenium Boot HTML report step timeline:

public final class CucumberStepLogger implements ConcurrentEventListener {

    private static final ThreadLocal<String> CURRENT_STEP = new ThreadLocal<>();

    @Override
    public void setEventPublisher(EventPublisher publisher) {
        publisher.registerHandlerFor(TestStepStarted.class, this::onStepStarted);
        publisher.registerHandlerFor(TestStepFinished.class, this::onStepFinished);
    }

    private void onStepStarted(TestStepStarted event) {
        if (event.getTestStep() instanceof PickleStepTestStep step) {
            CURRENT_STEP.set(step.getStepText());
        }
    }

    private void onStepFinished(TestStepFinished event) {
        String name = CURRENT_STEP.get();
        CURRENT_STEP.remove();
        if (name == null) return;
        StepLogger.step(name, mapStatus(event.getResult().getStatus()),
                        event.getResult().getStatus() == Status.FAILED);
    }
}
Enter fullscreen mode Exit fullscreen mode

IDE single-scenario execution

When you right-click a scenario in IntelliJ and click Run, the IDE uses its own Cucumber runner — not your CucumberRunner class — so @CucumberOptions is ignored and CucumberHooks is never discovered.

The fix: a cucumber.properties file. Cucumber reads it regardless of which runner launches the scenario:

# src/test/resources/cucumber.properties
cucumber.glue=com.yourcompany.bdd.steps,com.seleniumboot.cucumber
cucumber.plugin=pretty,com.seleniumboot.cucumber.CucumberStepLogger
cucumber.monochrome=true
Enter fullscreen mode Exit fullscreen mode

One file. No IDE configuration needed.


Parallel execution

Parallel safety comes for free. The driver is stored in a ThreadLocal via DriverManager, CucumberContext stores the Scenario in a ThreadLocal, and ExecutionMetrics uses a ConcurrentHashMap keyed by testId. Each scenario thread is fully isolated.

Enable parallel execution in selenium-boot.yml:

execution:
  parallel: methods
  threadCount: 4
  maxActiveSessions: 4
Enter fullscreen mode Exit fullscreen mode

What you get without writing it

By having com.seleniumboot.cucumber in your glue path, every scenario automatically gets:

  • Chrome or Firefox based on selenium-boot.yml — no WebDriverManager calls
  • Headless in CI — detected automatically
  • Screenshot on failure embedded in both the Selenium Boot HTML report and Cucumber's HTML report
  • Step names in the HTML report step timeline
  • Metrics tracked — duration, pass/fail, retry count
  • Parallel-safe ThreadLocal isolation
  • Self-healing locators if enabled
  • AI failure analysis if configured

The step definitions contain nothing but business logic.


Using it in Selenium Boot

All of this is built into Selenium Boot. Add the dependency and the two Cucumber JARs:

<dependency>
    <groupId>io.github.seleniumboot</groupId>
    <artifactId>selenium-boot</artifactId>
    <version>1.10.0</version>
</dependency>
<dependency>
    <groupId>io.cucumber</groupId>
    <artifactId>cucumber-java</artifactId>
    <version>7.20.1</version>
</dependency>
<dependency>
    <groupId>io.cucumber</groupId>
    <artifactId>cucumber-testng</artifactId>
    <version>7.20.1</version>
</dependency>
Enter fullscreen mode Exit fullscreen mode

Full docs at seleniumboot.github.io/selenium-boot/docs/cucumber.


How does your team currently handle WebDriver setup in Cucumber step definitions? Drop it in the comments.

Source: dev.to

arrow_back Back to Tutorials