Installing Cucumber with Java — Maven and JUnit/TestNG

9 min read

Cucumber is a collection of libraries, not a single download. Getting the right combination of artifacts into your Maven project — and wiring them so your IDE and mvn test both discover .feature files — is where most beginners spend their first frustrating hour. This lesson covers the exact pom.xml setup, project structure, and runner class you need to go from zero to first green scenario.

What you need on the classpath

A Cucumber + Java project needs three groups of dependencies working together:

  1. Cucumber runtime — parses .feature files, matches steps, runs the test lifecycle
  2. Test runner integration — bridges Cucumber to JUnit 5 or TestNG so mvn test and your IDE can discover and run scenarios
  3. Automation libraries — Selenium, Rest Assured, or whatever drives the system under test

The Cucumber runtime itself is always cucumber-java. The bridge depends on your runner choice.

<dependencies>
    <!-- Cucumber core runtime — step definition annotations, Gherkin parser -->
    <dependency>
        <groupId>io.cucumber</groupId>
        <artifactId>cucumber-java</artifactId>
        <version>7.18.0</version>
        <scope>test</scope>
    </dependency>
 
    <!-- Bridge: registers Cucumber as a JUnit Platform test engine -->
    <dependency>
        <groupId>io.cucumber</groupId>
        <artifactId>cucumber-junit-platform-engine</artifactId>
        <version>7.18.0</version>
        <scope>test</scope>
    </dependency>
 
    <!-- JUnit Platform Suite — enables the @Suite runner class -->
    <dependency>
        <groupId>org.junit.platform</groupId>
        <artifactId>junit-platform-suite</artifactId>
        <version>1.10.2</version>
        <scope>test</scope>
    </dependency>
 
    <!-- Selenium (for UI BDD in later chapters) -->
    <dependency>
        <groupId>org.seleniumhq.selenium</groupId>
        <artifactId>selenium-java</artifactId>
        <version>4.21.0</version>
        <scope>test</scope>
    </dependency>
 
    <!-- PicoContainer — DI for sharing state between step definition classes -->
    <dependency>
        <groupId>io.cucumber</groupId>
        <artifactId>cucumber-picocontainer</artifactId>
        <version>7.18.0</version>
        <scope>test</scope>
    </dependency>
</dependencies>

Keep all Cucumber artifacts at the same version. Mixing cucumber-java 7.18.0 with cucumber-junit-platform-engine 7.15.0 will cause subtle classpath conflicts that produce confusing errors. Use a Maven property: <cucumber.version>7.18.0</cucumber.version> and reference it as ${cucumber.version} in every Cucumber artifact.

Alternative: TestNG runner

If your project already uses TestNG (from a Selenium or Rest Assured course), swap the JUnit bridge for cucumber-testng:

<!-- Use instead of cucumber-junit-platform-engine -->
<dependency>
    <groupId>io.cucumber</groupId>
    <artifactId>cucumber-testng</artifactId>
    <version>7.18.0</version>
    <scope>test</scope>
</dependency>

The feature files and step definitions are identical. Only the runner class differs.

Maven Surefire plugin

Surefire must be at 3.x (or 2.22.2 minimum) to discover JUnit Platform tests:

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>3.2.5</version>
        </plugin>
    </plugins>
</build>

Without this, mvn test may silently find zero tests and report BUILD SUCCESS with nothing actually run — the classic false positive that wastes hours.

Project structure

Maven enforces src/test/java for test classes and src/test/resources for non-Java resources on the test classpath. Cucumber discovers .feature files from the classpath, so they belong under src/test/resources/:

bdd-project/
├── pom.xml
└── src/test/
    ├── java/
    │   ├── stepdefinitions/        ← @Given/@When/@Then classes
    │   ├── pages/                  ← Page Object Model classes
    │   ├── hooks/                  ← @Before/@After classes
    │   ├── context/                ← shared state objects (for DI)
    │   └── runners/                ← test runner class
    └── resources/
        ├── features/               ← .feature files
        │   ├── login.feature
        │   └── checkout.feature
        └── junit-platform.properties  ← JUnit Platform config

This layout is the same package discipline as a Selenium with Java or Rest Assured project — if you've done either of those courses, every folder name is already familiar.

The runner class (JUnit Platform)

The runner is a plain Java class with no methods — just annotations that configure Cucumber:

// src/test/java/runners/RunCucumberTest.java
package runners;
 
import org.junit.platform.suite.api.*;
import static io.cucumber.junit.platform.engine.Constants.*;
 
@Suite
@IncludeEngines("cucumber")
@SelectClasspathResource("features")
@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "stepdefinitions,hooks")
@ConfigurationParameter(key = PLUGIN_PROPERTY_NAME, value = "pretty, html:target/cucumber-reports.html")
public class RunCucumberTest { }

Key annotations:

  • @Suite — marks this as a JUnit Platform suite
  • @IncludeEngines("cucumber") — tells the suite to use the Cucumber engine
  • @SelectClasspathResource("features") — the classpath path to .feature files (resolves to src/test/resources/features/)
  • @ConfigurationParameter(key = GLUE_PROPERTY_NAME, ...) — the packages where Cucumber scans for step definitions and hooks; multiple packages comma-separated
  • @ConfigurationParameter(key = PLUGIN_PROPERTY_NAME, ...) — output plugins: pretty for console, html:path for an HTML report

The glue package is the most common misconfiguration. If Cucumber can't find your step definitions, check this first.

The runner class (TestNG)

With cucumber-testng, extend AbstractTestNGCucumberTests:

// src/test/java/runners/RunCucumberTest.java
package runners;
 
import io.cucumber.testng.AbstractTestNGCucumberTests;
import io.cucumber.testng.CucumberOptions;
 
@CucumberOptions(
    features = "src/test/resources/features",
    glue = {"stepdefinitions", "hooks"},
    plugin = {"pretty", "html:target/cucumber-reports.html"}
)
public class RunCucumberTest extends AbstractTestNGCucumberTests { }

@CucumberOptions is the TestNG equivalent of the JUnit Platform @ConfigurationParameter set.

How Cucumber executes a scenario

Step 1 of 7

Maven launches runner

Surefire finds RunCucumberTest; the JUnit Platform (or TestNG) discovers the Cucumber engine.

Running the tests

Three ways you'll use in practice:

  • IntelliJ: right-click RunCucumberTest → Run. You can also right-click a .feature file to run a single feature, or right-click a specific scenario line.
  • Maven CLI: mvn test runs everything. mvn test -Dcucumber.filter.tags="@smoke" runs only smoke-tagged scenarios.
  • IDE feature file: click the green play icon next to any scenario. IntelliJ integrates with both JUnit Platform and TestNG Cucumber runners.

⚠️ Common mistakes

  • Mismatched Cucumber artifact versions. All io.cucumber artifacts must share the same version. Pinning cucumber-java at 7.18.0 and cucumber-junit-platform-engine at 7.15.0 causes ClassNotFoundException or IncompatibleClassChangeError at runtime.
  • Wrong glue package. If the glue package in the runner doesn't match where your step definition classes actually live, every step is Undefined. Start with a single package (e.g., stepdefinitions) and expand.
  • Outdated Surefire. Maven's default Surefire version (2.x) predates JUnit Platform support. mvn test reports BUILD SUCCESS with zero tests run. Explicitly pin Surefire to 3.x in <build><plugins>.
  • .feature files in src/test/java/. Placing feature files next to Java source compiles fine in IntelliJ but they may not be on the classpath when Maven runs. Always put them under src/test/resources/.

🎯 Practice task

Get a Cucumber project building and a scenario running. 35–45 minutes.

  1. Create a new Maven project in IntelliJ (GroupId com.mycompany.bdd, ArtifactId bdd-framework).
  2. Add the four dependencies from this lesson: cucumber-java, cucumber-junit-platform-engine, junit-platform-suite, and cucumber-picocontainer. Pin the Surefire plugin at 3.2.5. Click "Load Maven changes".
  3. Create the package structure under src/test/java/: stepdefinitions, hooks, runners. Create src/test/resources/features/.
  4. Create runners/RunCucumberTest.java with the JUnit Platform annotations pointing glue at stepdefinitions,hooks.
  5. Create src/test/resources/features/hello.feature with one scenario: Given I have Cucumber set up / When I run the tests / Then I should see a green result. Run the runner — Cucumber will report Undefined steps with suggested snippets.
  6. Copy each snippet into a new class stepdefinitions/HelloSteps.java, implement each with System.out.println(...). Run again — all three steps should be green.
  7. Run mvn test from the terminal. Confirm BUILD SUCCESS and that target/cucumber-reports.html exists.
  8. Stretch: switch to the TestNG runner. Replace cucumber-junit-platform-engine and junit-platform-suite with cucumber-testng. Update the runner class to extend AbstractTestNGCucumberTests. Confirm the same scenario still runs green under TestNG.

Next lesson: feature files and Gherkin syntax in detail — every keyword you'll use in practice.

// tip to track lessons you complete and pick up where you left off across devices.