TestNG Runner Integration in Cucumber JVM
What Is TestNG Runner Integration?
TestNG Runner Integration is the process of connecting Cucumber JVM with the TestNG testing framework so that Cucumber feature files can be executed as part of a Java automation test suite. Cucumber provides the BDD layer: feature files, scenarios, Gherkin steps, step definitions, hooks, tags, plugins, and reports. TestNG provides the execution layer: it starts the test run, manages suite execution, supports parallelism, works with testng.xml, and integrates with Maven, Jenkins, and other CI/CD tools.
In simple terms, TestNG starts the execution and Cucumber executes the feature files. A feature file is a business-readable specification, but it does not run by itself in a Java project. Step definitions are Java methods, but they also need an execution engine. The TestNG runner class becomes the bridge. It tells TestNG to hand control to Cucumber, and it tells Cucumber where the feature files, glue code, tags, and reporting plugins are located.
This topic is important because many Selenium Java automation frameworks use TestNG as the primary test framework. TestNG is popular in enterprise automation because it supports flexible suite configuration, group-based execution, parameters, DataProviders, parallel execution, listeners, and rich reporting integration. When Cucumber is added to such a framework, TestNG Runner Integration allows BDD scenarios to run inside the same execution model.
Why Use TestNG with Cucumber?
JUnit is simple and widely used, but TestNG is often preferred in Selenium automation teams that need more control over execution. TestNG gives testers a practical way to organize large suites, run selected classes, pass parameters, configure listeners, and execute tests in parallel. When Cucumber is integrated with TestNG, teams can keep Gherkin scenarios business-readable while still using TestNG's strong execution features.
The main advantages include parallel scenario execution, better suite management through testng.xml, flexible configuration, data-driven support, listener integration, report customization, and CI/CD friendliness. These capabilities are useful when the automation suite grows beyond a few simple scenarios. A small Cucumber project may not need all of them, but enterprise frameworks usually benefit from a stable and configurable execution layer.
For example, a team may run only smoke scenarios during a deployment validation, run regression scenarios at night, run UI and API suites separately, and publish different reports in Jenkins. TestNG makes this kind of execution planning easier. Cucumber handles the BDD scenario execution, while TestNG controls when and how the runner is invoked.
Architecture Overview
The architecture of TestNG Runner Integration can be understood as a layered flow. TestNG sits at the top as the execution framework. The runner class extends a special Cucumber TestNG class. Cucumber then reads feature files, matches steps with Java step definitions, runs hooks, interacts with the application under test, and produces reports.
TestNG
-> Runner Class
-> AbstractTestNGCucumberTests
-> Cucumber Engine
-> Feature Files
-> Step Definitions
-> Hooks
-> Application
-> Reports
This flow explains most configuration problems. If TestNG cannot find the runner, nothing starts. If the runner does not extend the correct base class, feature files are not executed. If the feature path is wrong, scenarios are not discovered. If the glue package is wrong, steps become undefined. If plugins are missing, reports may not be generated. The runner is therefore a central part of execution, not just a decorative class.
Basic TestNG Runner Class
A basic TestNG-based Cucumber runner extends AbstractTestNGCucumberTests and uses @CucumberOptions to configure Cucumber execution. The class often does not contain normal test methods because the inherited Cucumber-TestNG bridge provides the execution mechanism.
import io.cucumber.testng.AbstractTestNGCucumberTests;
import io.cucumber.testng.CucumberOptions;
@CucumberOptions(
features = "src/test/resources/features",
glue = "stepdefinitions"
)
public class TestRunner extends AbstractTestNGCucumberTests {
}
The @CucumberOptions annotation tells Cucumber where the feature files are and where the glue code is. The extension of AbstractTestNGCucumberTests tells TestNG how to convert Cucumber scenarios into TestNG-executable test cases. Without that extension, TestNG sees only a normal Java class and does not know how to execute Gherkin scenarios.
Why Extend AbstractTestNGCucumberTests?
AbstractTestNGCucumberTests is the bridge between TestNG and Cucumber. It exposes Cucumber scenarios to TestNG through a DataProvider-based execution model. This is why TestNG can treat scenarios as test executions and why Cucumber scenarios appear in TestNG reports and CI results.
If a runner class does not extend this class, TestNG cannot execute feature files through it. The runner may compile, but the scenarios will not run as expected. This is a common beginner mistake. The class name may be correct, the feature path may be correct, and the step definitions may be correct, but the execution bridge is missing.
// Wrong
public class TestRunner {
}
// Correct
public class TestRunner extends AbstractTestNGCucumberTests {
}
In interviews, a strong answer is simple: AbstractTestNGCucumberTests connects TestNG with the Cucumber engine. It allows TestNG to trigger scenario execution and allows Cucumber results to participate in the TestNG lifecycle.
Main Runner Components
A practical TestNG runner usually contains feature location, glue package, tag filters, plugins, monochrome setting, dry run setting, publish setting, and sometimes parallel execution support. Each option has a specific responsibility. Feature location tells Cucumber where to find .feature files. Glue tells Cucumber where to find step definitions, hooks, and transformations. Tags select which scenarios should run. Plugins define output and reports.
@CucumberOptions(
features = "src/test/resources/features",
glue = {"stepdefinitions", "hooks"},
tags = "@Smoke",
plugin = {
"pretty",
"html:target/cucumber-report.html",
"json:target/cucumber.json"
},
monochrome = true,
dryRun = false
)
These options should stay focused on execution configuration. They should not contain application URLs, credentials, browser names, or environment-specific business data. Those values belong in configuration files, environment variables, Maven properties, or CI parameters. Keeping the runner clean makes the framework easier to maintain.
Feature Location and Glue Package
The feature location is commonly set to src/test/resources/features. This follows Maven's standard structure because feature files are test resources, not Java source code. Cucumber searches this folder and its subfolders for .feature files. If the path points to the wrong folder, Cucumber cannot discover scenarios.
The glue package is equally important. Glue is where Cucumber searches for step definitions, hooks, custom parameter types, and data table transformers. If the Java package is stepdefinitions but the runner says steps, Cucumber will report undefined steps even though the methods exist. Many Cucumber failures come from simple mismatches between folder/package names and runner configuration.
Tag Filtering in TestNG Runner
Tags are used to execute selected scenarios. A runner can execute only smoke scenarios by using tags = "@Smoke". More complex expressions can combine tags using logical operators.
@CucumberOptions(
tags = "@Regression and not @API"
)
This expression runs regression scenarios but excludes API scenarios. Tag filtering is powerful because it avoids editing feature files or creating temporary folders just to control execution. A team can mark scenarios with business, priority, technology, or execution-type tags and then use the runner or CI pipeline to select the required subset.
Use tags carefully. Too many tags create noise, and duplicate tag meanings make execution unreliable. A good tag strategy uses clear names such as @Smoke, @Regression, @UI, @API, @Payment, and @Critical. Tags should classify scenarios; they should not store test data or environment configuration.
Plugins and Reports
Cucumber plugins define console output and report files. The pretty plugin makes console output readable. The HTML plugin creates a human-friendly report. The JSON plugin is useful for advanced reporting tools. The JUnit XML plugin is useful for CI servers that understand JUnit-style results. The rerun plugin can store failed scenarios for later retry execution.
plugin = {
"pretty",
"html:target/cucumber-report.html",
"json:target/cucumber.json",
"junit:target/cucumber.xml",
"rerun:target/failed.txt"
}
Report paths should be consistent across runners. If smoke, regression, API, and UI runners write to inconsistent or overlapping locations, reports become harder to publish and compare. In CI/CD, reports should be generated under predictable folders such as target so Jenkins or another build tool can archive them.
Dry Run and Monochrome
dryRun = true validates step mappings without executing automation code. This is useful when developing a new feature file because it shows undefined steps and checks whether Gherkin lines are mapped. It should not be left enabled for normal test execution because no browser actions, API calls, or assertions will actually run.
monochrome = true makes console output cleaner by removing unnecessary color escape characters in terminals that do not render them properly. It does not affect scenario logic, but it improves readability in logs and CI output.
Running Through testng.xml
One reason teams choose TestNG is the ability to run suites through testng.xml. The XML file can define suites, tests, classes, parallel settings, parameters, and listeners. A simple TestNG XML file can point to a Cucumber runner class.
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="Smoke Suite">
<test name="Smoke Tests">
<classes>
<class name="runner.SmokeRunner"/>
</classes>
</test>
</suite>
In this flow, TestNG reads the XML suite, starts the runner class, and the runner starts Cucumber. This is useful for enterprise suites where different execution plans are needed. However, do not overuse runner classes. A few focused runners are helpful; dozens of nearly identical runners become hard to maintain.
Parallel Execution
Parallel execution is one of the strongest reasons to use TestNG with Cucumber. Cucumber TestNG integration can run scenarios in parallel by overriding the scenarios() DataProvider and setting parallel = true.
import org.testng.annotations.DataProvider;
public class TestRunner extends AbstractTestNGCucumberTests {
@Override
@DataProvider(parallel = true)
public Object[][] scenarios() {
return super.scenarios();
}
}
This can reduce execution time significantly, especially in Selenium suites with many independent scenarios. But parallel execution is not just a switch. The framework must be thread-safe. Static WebDriver fields, shared mutable state, common test data, and global variables can cause unpredictable failures when scenarios run at the same time.
For Selenium frameworks, prefer ThreadLocal WebDriver, scenario-specific context objects, isolated test data, and careful cleanup in hooks. If the framework is not thread-safe, parallel execution will create flaky tests instead of faster tests.
Multiple Runner Classes
Enterprise projects often create runner classes such as SmokeRunner, RegressionRunner, ApiRunner, and UiRunner. Each runner may use different tag filters or report outputs. This can be useful when those suites are stable and frequently executed.
@CucumberOptions(
features = "src/test/resources/features",
glue = {"stepdefinitions", "hooks"},
tags = "@Smoke",
plugin = {"pretty", "html:target/smoke-report.html"}
)
public class SmokeRunner extends AbstractTestNGCucumberTests {
}
Keep runner classes focused. They should not contain test logic, browser logic, data setup, or application workflows. Their job is execution configuration. Page objects, utilities, hooks, and step definitions should carry the automation implementation.
TestNG with Maven and Jenkins
In a Maven project, TestNG and Cucumber can be executed through the normal test lifecycle. A command such as mvn test starts Maven, Maven starts the test framework, TestNG starts the runner, and Cucumber executes scenarios. Maven can also pass tags, browser, environment, and other runtime values as properties.
mvn test -Dcucumber.filter.tags="@Smoke" -Denv=qa
In Jenkins, the same execution can be placed inside a pipeline stage. Jenkins pulls the code, runs Maven, executes TestNG, triggers Cucumber, and publishes reports. A typical flow is Git checkout, Maven test, TestNG runner, Cucumber execution, report generation, and report publication. This standard flow is why TestNG integration is valuable in real automation teams.
Common Mistakes
Forgetting the Base Class
The most common mistake is creating a runner class but not extending AbstractTestNGCucumberTests. Without the extension, TestNG cannot execute Cucumber scenarios through that runner.
Wrong Feature Path
Another common mistake is pointing to the wrong folder. Feature files should usually be under src/test/resources/features. If they are placed under src/test/java, the project structure becomes confusing and resource discovery may break.
Wrong Glue Package
If the glue path does not match the actual Java package, step definitions and hooks will not be found. This creates undefined step errors even when the code exists.
Ignoring Thread Safety
Enabling parallel execution without thread-safe WebDriver and scenario state is risky. Static driver fields and shared mutable objects often cause flaky failures in parallel runs.
Hardcoding Environment Data
URLs, credentials, browser names, and environment names should not be hardcoded in runner classes. Keep environment configuration external so the same runner can work locally, in QA, in staging, and in CI.
Best Practices
Always extend AbstractTestNGCucumberTests in TestNG-based Cucumber runners. Keep runner classes small and focused on execution configuration. Store feature files under src/test/resources/features and organize them by business module. Keep glue packages clear and include hooks in the glue configuration.
Use meaningful tags for selective execution rather than modifying feature files or feature paths repeatedly. Configure only the plugins your project needs. Generate reports in predictable locations. Use testng.xml when suite-level control is useful, but avoid creating unnecessary runner duplication.
Enable parallel execution only after the framework is thread-safe. Use ThreadLocal WebDriver or a dependency injection approach. Clean up driver instances, test data, and scenario context after each scenario. A fast unstable suite is worse than a slower reliable suite.
Dependency Setup in a Real Project
A TestNG-based Cucumber JVM project needs compatible dependencies. The usual set includes Cucumber Java, Cucumber TestNG, TestNG, and Selenium when the scenarios automate a browser. Maven or Gradle is normally used to control versions. Version compatibility matters because Cucumber modules should generally come from the same release line. Mixing a newer Cucumber Java dependency with an older Cucumber TestNG dependency can cause confusing runtime issues.
A simplified Maven setup usually includes dependencies for io.cucumber:cucumber-java, io.cucumber:cucumber-testng, org.testng:testng, and org.seleniumhq.selenium:selenium-java when UI automation is involved. Larger projects may add WebDriverManager, logging libraries, reporting libraries, assertion libraries, REST clients, JSON parsers, and dependency injection tools. These extra tools support the framework, but the runner integration itself depends on the Cucumber-TestNG bridge.
When debugging dependency issues, check three things first. Confirm that the Cucumber modules use compatible versions. Confirm that TestNG is available in the test scope. Confirm that the build plugin actually runs TestNG tests. Sometimes the runner class is correct, but Maven Surefire or the IDE configuration does not discover it because naming patterns or suite settings are wrong.
TestNG Listeners and Cucumber Reports
TestNG listeners are often used in Selenium frameworks for logging, screenshots, retry logic, and custom reporting. With Cucumber, use listeners carefully. Cucumber already has hooks and plugins that understand scenario execution. TestNG listeners understand TestNG test methods. Because Cucumber scenarios are exposed through TestNG's execution model, listeners may still work, but they should not duplicate responsibilities already handled better by Cucumber hooks.
For example, screenshot-on-failure is usually clearer in a Cucumber @After hook because the hook receives scenario context and can attach evidence to a Cucumber report. A TestNG listener may be useful for suite-level reporting or retry behavior, but scenario-level behavior is often better handled through Cucumber hooks. The best framework design uses the right extension point for the right job.
Reports also need a clear strategy. Cucumber HTML and JSON reports describe scenarios and steps. TestNG default reports describe TestNG execution. Extent Reports or other custom reports may combine information from both. Before adding reporting layers, decide what the team needs to see: scenario names, steps, tags, screenshots, duration, browser, environment, failure reason, and rerun status. Reporting should support debugging, not just create attractive files.
ThreadLocal WebDriver in Parallel Runs
Parallel execution is powerful only when each scenario has isolated browser state. A common safe pattern is to store WebDriver in a ThreadLocal. Each parallel thread gets its own driver instance, and step definitions retrieve the driver for the current thread. This prevents one scenario from accidentally using another scenario's browser.
public class DriverManager {
private static final ThreadLocal<WebDriver> driver = new ThreadLocal<>();
public static void setDriver(WebDriver webDriver) {
driver.set(webDriver);
}
public static WebDriver getDriver() {
return driver.get();
}
public static void removeDriver() {
driver.remove();
}
}
The setup hook can create the driver and store it in ThreadLocal. The teardown hook can quit and remove it. This pattern is not the only valid approach, but it demonstrates the key principle: browser state should not be shared across parallel scenarios. The same idea applies to scenario data, API tokens, temporary files, and database records. Parallel-safe frameworks isolate state and clean up consistently.
Choosing Between Runner Tags and CI Parameters
Teams often start by hardcoding tags in runner classes. This is simple, but it can become limiting. If every new execution type requires a new runner, the project fills with nearly identical classes. A better approach is to keep the runner stable and let CI pass tag expressions when possible.
For stable suites such as smoke and regression, dedicated runners may be acceptable because those suites are part of the team's normal process. For temporary selections such as one module, one defect fix, or one sprint-specific run, command-line properties are cleaner. This keeps version-controlled runner files stable and makes pipelines more flexible.
In real projects, the best design is usually mixed. Keep a small number of meaningful runners for common suites, and allow Maven, TestNG XML, or CI variables to override tags for temporary execution. This avoids both extremes: one giant runner that is hard to control and dozens of duplicate runners that are hard to maintain.
Interview-Ready Summary
TestNG Runner Integration enables Cucumber feature files to execute through the TestNG framework. The runner class extends AbstractTestNGCucumberTests, which connects TestNG with the Cucumber engine. The runner uses @CucumberOptions to configure feature paths, glue packages, tags, plugins, dry run, monochrome output, and reporting.
TestNG is useful with Cucumber because it offers advanced suite management, parallel execution, flexible configuration, listeners, and CI/CD integration. Parallel execution can be enabled by overriding the scenarios() DataProvider, but the framework must be thread-safe before this is used in real projects.
The golden rule is simple: TestNG starts the execution, the runner connects TestNG to Cucumber, and Cucumber executes the BDD scenarios. Correct feature paths, glue configuration, tag strategy, reporting plugins, and thread safety make the integration reliable and scalable.