@CucumberOptions Explained in Cucumber JVM

What Is @CucumberOptions?

@CucumberOptions is a configuration annotation used in Cucumber JVM projects, mainly with JUnit 4 and TestNG runner classes. It tells Cucumber what to execute and how to execute it. Through this annotation, a framework can specify feature file locations, glue packages, tag filters, report plugins, console formatting, dry run behavior, publishing, and generated step definition snippet style.

Without configuration, Cucumber may not know where the feature files are stored, where the Java step definitions are located, which scenarios should be selected, or which reports should be generated. @CucumberOptions centralizes those execution instructions in the runner class. It does not contain business logic; it describes the execution setup.

This annotation is especially common in older JUnit 4 frameworks and TestNG-based Selenium frameworks. In newer JUnit 5 projects, configuration is usually handled through the JUnit Platform engine, suite annotations, configuration parameters, or junit-platform.properties. Still, @CucumberOptions remains an important interview and real-project topic because many enterprise frameworks use it.

Why Is @CucumberOptions Needed?

Cucumber projects contain different kinds of files. Feature files live under test resources. Step definition classes live under test Java packages. Hooks may be in separate packages. Reports are written under target folders. Tags are used to select scenarios. A runner needs to connect these pieces. @CucumberOptions provides that connection.

Think of the runner class as the entry point and @CucumberOptions as the instruction board. The runner starts Cucumber, and the options tell it where to look, what to filter, and how to report. If one option is wrong, the suite may compile but still fail at runtime. Wrong feature paths lead to no scenarios. Wrong glue paths lead to undefined steps. Wrong tag expressions lead to unexpected scenario selection.

Where Is @CucumberOptions Used?

@CucumberOptions is placed above the Cucumber runner class. In a JUnit 4 project, it is commonly used with @RunWith(Cucumber.class). In a TestNG project, it is commonly used on a class that extends AbstractTestNGCucumberTests.

import org.junit.runner.RunWith;
import io.cucumber.junit.Cucumber;
import io.cucumber.junit.CucumberOptions;

@RunWith(Cucumber.class)
@CucumberOptions(
    features = "src/test/resources/features",
    glue = "stepdefinitions"
)
public class TestRunner {
}

For TestNG, the same annotation can configure the Cucumber side of execution while TestNG provides the execution engine.

import io.cucumber.testng.AbstractTestNGCucumberTests;
import io.cucumber.testng.CucumberOptions;

@CucumberOptions(
    features = "src/test/resources/features",
    glue = "stepdefinitions"
)
public class TestRunner extends AbstractTestNGCucumberTests {
}

Execution Flow

The execution flow begins when the runner class is launched from an IDE, Maven, Gradle, or CI pipeline. The test framework starts the runner. Cucumber reads @CucumberOptions. It locates feature files, scans glue packages, applies tag filters, executes hooks and scenarios, and finally generates the configured reports.

Runner Class
  -> @CucumberOptions
  -> Locate Feature Files
  -> Locate Glue Code
  -> Apply Tags
  -> Execute Scenarios
  -> Generate Reports

This sequence is useful for debugging. If feature files are not found, inspect the features option. If steps are undefined, inspect the glue option. If scenarios are skipped unexpectedly, inspect the tags option. If reports are missing, inspect the plugin option.

Common Attributes

AttributePurpose
featuresDefines the feature file or folder location
glueDefines step definition, hook, and support packages
tagsSelects scenarios using tag expressions
pluginConfigures console output and report formats
monochromeMakes console output cleaner
dryRunChecks step mappings without executing automation code
publishPublishes reports when configured
snippetsControls undefined step snippet naming style

The features Option

The features option tells Cucumber where to find feature files. The recommended Maven location is usually src/test/resources/features. Cucumber can point to an entire folder, a subfolder, one feature file, or multiple locations.

@CucumberOptions(
    features = "src/test/resources/features"
)

When this path points to a folder, Cucumber searches for all .feature files under that folder. In larger projects, feature files may be organized into module folders such as login, payment, customer, order, or reports. Pointing to the root features folder allows Cucumber to discover all of them recursively.

@CucumberOptions(
    features = {
        "src/test/resources/features/login",
        "src/test/resources/features/payment"
    }
)

The feature path selects files. It should not be used as the main way to select scenarios every day. For most selective execution, tags are better because they operate at the scenario level and do not require changing folder paths.

The glue Option

The glue option tells Cucumber where Java support code lives. Glue includes step definitions, hooks, parameter types, and data table transformations. If Cucumber can read the feature file but cannot find matching step definitions, the glue package is the first place to check.

@CucumberOptions(
    glue = {"stepdefinitions", "hooks", "common"}
)

In real frameworks, use package names that reflect the actual Java package structure. If step definitions are under com.company.project.steps, the glue should point to that package or a common parent package. Avoid scanning the entire codebase if not needed because it makes configuration vague and may slow down discovery.

The tags Option

The tags option controls selective execution. It lets a team run smoke tests, regression tests, module tests, UI tests, API tests, or critical tests without changing feature files. A simple expression can run only smoke scenarios.

@CucumberOptions(
    tags = "@Smoke"
)

Complex tag expressions can combine conditions using and, or, and not.

@CucumberOptions(
    tags = "@Regression and not @API"
)

This runs regression scenarios but excludes API scenarios. Tag expressions should be meaningful and stable. Avoid temporary or vague tags such as @Test1, @Run, or @MyScenario. Tags should support execution, reporting, ownership, or CI/CD strategy.

The plugin Option

The plugin option configures output and reporting. The pretty plugin prints readable console output. HTML reports are useful for human review. JSON reports are useful for report aggregation tools. JUnit XML reports are useful for CI servers. The rerun plugin stores failed scenarios for retry execution.

@CucumberOptions(
    plugin = {
        "pretty",
        "html:target/cucumber-report.html",
        "json:target/cucumber.json",
        "junit:target/cucumber.xml",
        "rerun:target/failed.txt"
    }
)

Do not add every plugin just because it exists. Generate the reports the project actually uses. Too many report outputs can slow execution, clutter build folders, and confuse CI publishing. A practical setup usually includes readable console output, one human-readable report, and one machine-readable report.

monochrome, dryRun, publish, and snippets

monochrome = true makes console logs cleaner by removing color escape noise in terminals that do not support formatting. It is not required for scenario execution, but it improves readability in CI logs and local terminals.

dryRun = true validates whether every Gherkin step has a matching step definition. It does not run automation code, so no browser actions, API calls, or assertions happen. This is useful during framework development but dangerous if accidentally left enabled in normal execution.

publish = true enables Cucumber report publishing when the project is configured for it. Teams should understand privacy and data-sharing expectations before publishing reports externally. Internal enterprise projects often prefer local HTML, JSON, or CI-hosted reports.

The snippets option controls how undefined step definition snippets are generated. Some teams prefer camel case method names, while others prefer underscore style. This is mostly a consistency choice.

@CucumberOptions(
    snippets = io.cucumber.core.snippets.SnippetType.CAMELCASE
)

Complete Runner Example

A practical runner combines the most important options while staying focused on execution configuration.

@RunWith(Cucumber.class)
@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,
    publish = false
)
public class SmokeRunner {
}

This runner searches for feature files, loads step definitions and hooks, selects smoke scenarios, prints readable output, creates HTML and JSON reports, and executes real automation code because dryRun is false.

@CucumberOptions vs JUnit Platform Configuration

@CucumberOptions is common with JUnit 4 and TestNG. Modern JUnit 5 projects usually use the JUnit Platform engine instead. That means configuration may move to suite annotations, configuration parameters, junit-platform.properties, Maven properties, or Gradle settings.

Aspect@CucumberOptionsJUnit Platform
Common usageJUnit 4 and TestNGJUnit 5 projects
StyleAnnotation on runner classSuite annotations and properties
Feature selectionfeatures option@SelectClasspathResource
Glue configurationglue optionConfiguration parameter
Modern recommendationUseful in many existing projectsPreferred for new JUnit 5 frameworks

A strong engineer should understand both styles. If you join an older Selenium framework, you may see @CucumberOptions. If you create a new JUnit 5 framework, you may use platform configuration instead.

Common Mistakes

Wrong Feature Path

Using features = "feature" when the folder is actually src/test/resources/features prevents discovery. Always compare the runner path with the real project structure.

Wrong Glue Package

If the glue package does not match the Java package, Cucumber will not find step definitions or hooks. Undefined steps often mean glue configuration is wrong, not that the feature file is wrong.

Leaving dryRun Enabled

If dryRun = true remains enabled, scenarios are checked but not executed. This can make a build appear fast while no real validation happened.

Too Many Plugins

Generating every possible report creates clutter. Choose reports based on project needs and CI/CD publishing requirements.

Hardcoding Environment Values

Do not put URLs, credentials, browser names, or environment names into @CucumberOptions. Use configuration files, Maven properties, environment variables, or CI parameters.

Best Practices

Keep @CucumberOptions readable. Configure feature paths, glue packages, tags, plugins, and execution flags, but do not add business logic. Store feature files under src/test/resources/features. Keep glue packages organized. Use tag expressions for scenario selection. Keep report output paths consistent across runners.

Use dryRun during development to validate step mappings, but disable it for real execution. Use only required plugins. Prefer CI-controlled tag expressions for temporary execution needs. For new JUnit 5 projects, consider JUnit Platform configuration instead of @CucumberOptions.

Keeping Runner Configuration Separate from Environment Configuration

One of the most important design rules is to keep Cucumber execution configuration separate from application environment configuration. @CucumberOptions should define where Cucumber finds features, glue, tags, and reports. It should not define the QA URL, staging URL, browser name, username, password, grid address, or database connection. Those values change across environments and should live in configuration files, system properties, environment variables, or CI secrets.

This separation improves maintainability. The same runner can work locally, in Jenkins, in GitHub Actions, or in a release pipeline. The runner stays stable while execution properties change from the outside. If environment details are hardcoded in runner classes, every environment change becomes a code change, and teams may accidentally commit sensitive data or break another environment's execution.

A clean framework may pass values like this: mvn test -Denv=qa -Dbrowser=chrome -Dcucumber.filter.tags="@Smoke". The runner handles Cucumber discovery. The framework's configuration layer reads env and browser. This design keeps responsibilities clear.

Using @CucumberOptions in CI/CD

In CI/CD pipelines, @CucumberOptions should support predictable discovery and reporting. Stable feature and glue paths help every build agent run the same suite structure. Consistent report paths allow Jenkins, Azure DevOps, GitHub Actions, or other CI tools to publish artifacts after execution. Tag filters can be hardcoded for stable runners or passed through build properties for dynamic execution.

A pull request pipeline may run smoke tests. A nightly pipeline may run regression. A release pipeline may run critical scenarios. The runner can stay the same if the pipeline passes tag expressions externally. This avoids editing the runner for each pipeline stage. When using TestNG runners, testng.xml can also select runner classes and suite-level settings.

CI pipelines should fail clearly when no scenarios are found or when all scenarios are filtered out unexpectedly. Silent empty execution is dangerous because it gives false confidence. Good teams review logs, report counts, and scenario totals to confirm that the intended suite actually ran.

Rerun Plugin and Failed Scenario Strategy

The rerun plugin writes failed scenario locations to a file. This can be useful when dealing with large suites because failed scenarios can be rerun separately after the first execution. A common configuration is rerun:target/failed.txt. After the initial run, another runner or command can use that file to rerun failures.

However, rerun should not be used to hide flaky tests. If scenarios pass only after repeated attempts, the team should investigate synchronization, test data, environment instability, or framework design. Rerun is helpful for temporary instability and evidence collection, but it is not a substitute for reliable automation.

When using rerun in a professional pipeline, make the behavior explicit. Report both original failures and rerun results. If the final report shows only the rerun pass, the team may miss instability trends. Good reporting makes flakiness visible instead of burying it.

Designing Multiple Runners

Many frameworks create multiple runner classes for smoke, regression, API, UI, and module-specific execution. This is acceptable when each runner represents a stable and meaningful suite. The problem begins when every short-term need becomes a new runner. Duplicate runners make maintenance harder because feature paths, glue packages, and plugins must be updated in many places.

A practical rule is to keep a small number of permanent runners and use tags or CI parameters for temporary selection. If several runners share the same feature path, glue package, and plugins, but differ only by tag, consider whether the tag should be passed from the command line instead. This keeps runner classes from becoming clutter.

Runner classes should be reviewed like other framework code. If a runner is no longer used, remove it. If two runners do the same thing, merge them. If plugin paths are inconsistent, standardize them. Execution configuration deserves the same discipline as page objects, step definitions, and utilities.

Interview-Ready Summary

@CucumberOptions is a Cucumber JVM configuration annotation used mainly with JUnit 4 and TestNG runners. It controls feature file location, glue packages, tag filters, reporting plugins, dry run behavior, monochrome output, publishing, and snippet generation. It tells Cucumber what to execute and how to execute it.

The most important attributes are features, glue, tags, and plugin. Correct feature and glue paths are essential for discovery and step matching. Tags support selective execution. Plugins generate output and reports. The golden rule is to keep execution configuration in the runner and keep application configuration outside it.