How to Combine Selenium, Cucumber, and TestNG for Automation Testing
Combine Selenium WebDriver, Cucumber-JVM, and TestNG in a Java Maven project. Set up a runner, write scenarios and steps, then scale execution safely.
Direct answer: use Selenium WebDriver to control the browser, Cucumber-JVM to express behavior in Gherkin and bind it to Java step definitions, and Cucumber’s TestNG integration to discover and execute scenarios. They handle different jobs. Selenium does not provide assertions or test execution; Cucumber does not include an assertion library. The example below uses Maven and TestNG’s serial runner, then shows how to enable parallel scenarios.
1. Understand the roles
| Component | Responsibility |
|---|---|
| Selenium WebDriver | Find elements, interact with the browser, and read the resulting page state. |
| Cucumber-JVM | Load Gherkin feature files and match Given/When/Then steps to Java glue code. |
| TestNG | Run the Cucumber scenarios through the Cucumber TestNG runner and provide execution configuration. |
| Assertion library | Decide whether observed results meet expectations. Add TestNG assertions or another assertion library explicitly. |
| Selenium Grid (optional) | Route browser sessions to remote machines, browser versions, or platforms. |
This separation is intentional: Selenium’s documentation explains that WebDriver communicates with the browser but does not compare results, determine pass or fail, or understand Gherkin grammar (Selenium components). Cucumber documents browser automation using Selenium, and its TestNG module supplies the runner integration (Cucumber browser automation, Cucumber TestNG documentation).
2. Create the Maven project
Use a current supported JDK and check the official Selenium download guidance for a compatible current Selenium Java version. Likewise, use one matching version for all Cucumber artifacts. Version numbers change, so put the selected values in Maven properties rather than copying a stale tutorial version.
selenium.version=CURRENT_SELENIUM_VERSION
cucumber.version=CURRENT_CUCUMBER_VERSION
testng.version=CURRENT_TESTNG_VERSION
Replace these placeholders in the POM below with versions checked against the official project documentation before running it. The Selenium install guide documents the Maven selenium-java artifact (Selenium library installation).
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>example</groupId>
<artifactId>ui-acceptance-tests</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<selenium.version>CURRENT_SELENIUM_VERSION</selenium.version>
<cucumber.version>CURRENT_CUCUMBER_VERSION</cucumber.version>
<testng.version>CURRENT_TESTNG_VERSION</testng.version>
</properties>
<dependencies>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>${selenium.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-java</artifactId>
<version>${cucumber.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-testng</artifactId>
<version>${cucumber.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>${testng.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>CURRENT_SUREFIRE_VERSION</version>
<configuration>
<includes>
<include>**/RunCucumberTest.java</include>
</includes>
</configuration>
</plugin>
</plugins>
</build>
</project>
The Maven Surefire plugin version is also a placeholder: select a current release and confirm its runner discovery conventions. Cucumber documents running through Maven Surefire or Failsafe (Cucumber parallel execution). Use Failsafe if these are integration tests that should run in Maven’s integration-test/verify lifecycle; configure its includes and lifecycle accordingly.
3. Lay out features, glue, and the runner
src/
test/
java/
example/
ui/
RunCucumberTest.java
Hooks.java
LoginSteps.java
resources/
features/
login.feature
The runner’s glue points to the Java package containing step definitions and hooks. Its features value points to the feature resource location.
package example.ui;
import io.cucumber.testng.AbstractTestNGCucumberTests;
import io.cucumber.testng.CucumberOptions;
@CucumberOptions(
features = "src/test/resources/features",
glue = "example.ui",
plugin = {"pretty", "html:target/cucumber-report.html"}
)
public class RunCucumberTest extends AbstractTestNGCucumberTests {
}
This is the serial form: inherit the base class without overriding its data provider. The feature file can describe a user-visible behavior:
Feature: Sign in
Scenario: A registered user signs in
Given I open the sign-in page
When I sign in as "reader@example.test" with password "correct-horse"
Then I should see the account page
Keep credentials and environment-specific URLs out of committed feature files. Use test configuration or environment variables for real environments; use synthetic credentials and a controlled test environment for examples.
4. Start and stop a browser reliably
Hooks own per-scenario setup and teardown. Selenium Manager, included with Selenium bindings, handles driver management by default. A local Chrome example is:
package example.ui;
import io.cucumber.java.After;
import io.cucumber.java.Before;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
public class Hooks {
private static final ThreadLocal<WebDriver> DRIVER = new ThreadLocal<>();
@Before
public void startBrowser() {
DRIVER.set(new ChromeDriver());
}
@After
public void stopBrowser() {
WebDriver driver = DRIVER.get();
if (driver != null) {
try {
driver.quit();
} finally {
DRIVER.remove();
}
}
}
static WebDriver driver() {
WebDriver driver = DRIVER.get();
if (driver == null) {
throw new IllegalStateException("Browser is not initialized for this scenario");
}
return driver;
}
}
The ThreadLocal shape supports scenario-local access when scenarios run on different threads. In a serial-only project, an ordinary scenario-scoped context object is also suitable. Do not keep a mutable driver in a shared static field: parallel scenarios could then control or close each other’s browser.
5. Implement step definitions and assertions
package example.ui;
import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;
import org.openqa.selenium.By;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.testng.Assert;
import java.time.Duration;
public class LoginSteps {
private static final String BASE_URL =
System.getProperty("baseUrl", "https://example.test");
@Given("I open the sign-in page")
public void openSignInPage() {
Hooks.driver().get(BASE_URL + "/login");
}
@When("I sign in as {string} with password {string}")
public void signIn(String email, String password) {
Hooks.driver().findElement(By.name("email")).sendKeys(email);
Hooks.driver().findElement(By.name("password")).sendKeys(password);
Hooks.driver().findElement(By.cssSelector("button[type='submit']")).click();
}
@Then("I should see the account page")
public void shouldSeeAccountPage() {
new WebDriverWait(Hooks.driver(), Duration.ofSeconds(10))
.until(ExpectedConditions.urlContains("/account"));
Assert.assertTrue(
Hooks.driver().getCurrentUrl().contains("/account"),
"Expected sign-in to navigate to the account page");
}
}
The domain and selectors are illustrative and must match the application under test. Use explicit waits for observable conditions rather than fixed sleeps. Cucumber does not bundle an assertion library, so this example uses TestNG’s Assert explicitly (Cucumber-JVM documentation).
6. Run the suite and filter scenarios
- Replace all version placeholders in the POM with compatible current releases.
- Put a feature in the configured resource path and step classes in the configured glue package.
- Run
mvn test -DbaseUrl=https://your-test-environment.example. - Review the console output and
target/cucumber-report.html.
For selective execution, add Cucumber tags to features, for example @smoke, and pass a tag expression using a runner option or the Cucumber properties supported by the selected Cucumber version. Confirm the current property name in Cucumber’s documentation before wiring CI. Tags are useful for suite selection; they do not replace stable test data or scenario isolation.
7. Enable parallel scenarios when the suite is isolated
Cucumber’s TestNG integration can run scenarios and Scenario Outline rows in parallel by overriding the inherited data provider:
package example.ui;
import io.cucumber.testng.AbstractTestNGCucumberTests;
import io.cucumber.testng.CucumberOptions;
import org.testng.annotations.DataProvider;
@CucumberOptions(
features = "src/test/resources/features",
glue = "example.ui",
plugin = {"pretty", "html:target/cucumber-report.html"}
)
public class RunCucumberTest extends AbstractTestNGCucumberTests {
@Override
@DataProvider(parallel = true)
public Object[][] scenarios() {
return super.scenarios();
}
}
Start with serial execution, then increase parallelism only after each scenario has its own browser session, independent test account or data, and isolated mutable fixtures. Ensure external systems can handle the concurrent load. Cucumber’s guide describes this TestNG data-provider pattern and Maven execution options (Cucumber parallel execution).
8. Add remote browsers with Selenium Grid
Keep the same feature files and step definitions; change driver construction to use a remote WebDriver endpoint and the desired browser capabilities. Grid routes WebDriver commands to remote browser instances and is useful for remote, distributed, or cross-browser execution (Selenium Grid).
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.remote.RemoteWebDriver;
import java.net.URI;
WebDriver driver = new RemoteWebDriver(
URI.create(System.getProperty("gridUrl")).toURL(),
new ChromeOptions()
);
Adapt the hook to choose local or remote construction through configuration. Treat the Grid URL and credentials as deployment settings, and close each remote session with quit(). Grid is optional for a small local suite; it adds operational setup and should be introduced when local capacity or browser/platform coverage calls for it.
9. State, data, and maintainability
- Keep each scenario understandable and independently runnable.
- Use page objects or small screen abstractions when they make repeated browser operations easier to maintain.
- Keep state scenario-scoped. Cucumber creates fresh glue instances per scenario; when multiple glue classes need shared scenario state, use a dependency-injection integration rather than static mutable variables.
- PicoContainer is Cucumber’s documented recommendation when the application does not already use another DI module; Spring and Guice integrations are also available (Cucumber state and dependency injection).
- Use stable test fixtures and clean up created data where practical, especially when retries or parallel runs are enabled.
10. Common problems and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| No tests or scenarios run | Maven did not discover the runner, or the feature path is wrong. | Check the runner filename and Surefire/Failsafe includes; verify features points to the resources and rerun with Maven output visible. |
| Undefined steps | The glue package does not include the step class, or the expression does not match the feature text. | Correct glue and package names; align the step annotation text and parameter types with the Gherkin step. |
| Dependency or class-loading conflict | Cucumber modules use different versions or incompatible dependency versions are resolved. | Align all Cucumber artifacts to one version, inspect Maven’s dependency tree, and consult current official compatibility guidance. |
| Driver or browser startup failure | Browser installation, permissions, environment, or browser/driver compatibility issue. | Check that the browser is installed and runnable in the execution environment; use Selenium Manager’s supported defaults or configure the environment per Selenium’s current setup guide. |
| Element not found or intermittent click failure | Locator changed, page has not reached the expected state, or an overlay intercepts interaction. | Check the selector against the rendered page and wait for the specific element/state with an explicit wait. |
| Assertions fail despite successful navigation | The assertion checks the wrong observable condition, or the application redirects differently. | Assert the intended page state or content and inspect the actual URL and page output in the failed run. |
| Parallel-only failures | Scenarios share a driver, account, data record, download path, or other mutable resource. | Isolate browser and test data per scenario; make filenames unique; reduce concurrency until shared state is removed. |
| Report output is missing or overwritten | Wrong plugin path, runner not executed, or multiple concurrent runs write to the same destination. | Confirm the Cucumber plugin configuration and use run-specific report destinations where concurrent builds share a workspace. |
11. Performance, reliability, and cost
Browser startup and page work usually dominate a UI scenario’s runtime. Reuse a browser within a scenario, but keep separate scenarios isolated; avoid arbitrary sleeps and unnecessary page reloads. Parallel data providers can reduce elapsed suite time when machine capacity and the application support concurrency, while also increasing browser CPU and memory use. Measure the suite in its own CI environment before choosing a concurrency level.
For reliability, prefer explicit waits, stable selectors, controlled test data, deterministic environments, and cleanup that runs even after failures. Retries can help diagnose transient infrastructure failures, but repeated retries can hide defects; preserve failure output and distinguish infrastructure errors from product failures. A remote Grid can expand browser coverage and execution capacity, with added network and service dependencies.
The software stack itself has no per-screenshot charge in this setup; compute, browser infrastructure, and any remote execution service have costs determined by the environment chosen. The dossier provides no benchmark or provider pricing to compare, so size infrastructure from observed workload rather than assumed speedups.
Or skip the browser setup
If the task is to capture a page image or PDF rather than verify an interactive workflow, ScreenshotNeo offers a website screenshot API and MCP server. One GET request can return PNG, JPEG, WebP, or PDF. See the ScreenshotNeo API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Cookie banners are accepted before capture and 60+ known consent platforms, newsletter popups, and chat widgets are removed; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots monthly with no card; paid plans start at $5 for 3,000, and every feature is on every plan. Sign up for 1,000 free screenshots a month, with no card.
FAQ
Can I use Cucumber without TestNG?
Yes. Cucumber has other execution integrations. This guide uses TestNG specifically to run scenarios through its Cucumber integration.
Does TestNG replace Selenium?
No. TestNG executes the suite; Selenium still performs browser interactions.
Do I need Selenium Grid?
No. Start locally and introduce Grid when remote execution, more capacity, or broader browser and platform coverage is needed.
Can I parallelize Scenario Outline examples?
Yes. Cucumber’s TestNG data provider can run scenarios and Scenario Outline rows in parallel when configured accordingly; isolate their browsers and data first.


