How do you declare a Cucumber-JVM suite on the JUnit Platform, and how does it find the feature files?
answer
- The class declares; it does not test
- Cucumber runs as its own platform engine
- Features are resources, not Java sources
- One annotation includes an engine by id
- A selector names a classpath resource directory
basics
~20 sAnnotate a plain class with the JUnit Platform's @Suite and @IncludeEngines("cucumber"), then point it at the feature resources with @SelectClasspathResource. Cucumber's JUnit Platform engine must be on the test classpath; it then creates one test per scenario.
solid answer
~40 sA Cucumber-JVM suite class holds no test methods — it is a declaration. `@Suite` marks it as a JUnit Platform suite, `@IncludeEngines("cucumber")` restricts the nested run to Cucumber's own test engine (shipped in the `cucumber-junit-platform-engine` artifact), and a selector such as `@SelectClasspathResource("com/vinylmarket/acceptance")` tells that engine which classpath directory holds the `.feature` files. Everything else — glue packages, tag filter, parallelism — is set with `@ConfigurationParameter` using Cucumber's `cucumber.*` keys, or left in a properties file. The engine reports **one executable test per scenario**, with every `Examples` row of a Scenario Outline counted as its own scenario, so Maven, Gradle and the IDE all show a per-scenario tree. The classic first failure is putting `.feature` files under `src/test/java`, where the build never copies them onto the test classpath; they belong under `src/test/resources`.
code
java · 15 linespackage com.vinylmarket.acceptance;
import org.junit.platform.suite.api.ConfigurationParameter;
import org.junit.platform.suite.api.IncludeEngines;
import org.junit.platform.suite.api.SelectClasspathResource;
import org.junit.platform.suite.api.Suite;
import static io.cucumber.junit.platform.engine.Constants.GLUE_PROPERTY_NAME;
@Suite
@IncludeEngines("cucumber")
@SelectClasspathResource("com/vinylmarket/acceptance")
@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "com.vinylmarket.acceptance.steps")
public class RunCucumberTest {
}go deeper
Be ready to write the suite class from memory and to say where feature files live in a Maven or Gradle layout. Knowing that the class carries no test methods is most of the bar at this level.
Explain what each annotation contributes — suite declaration, engine selection by id, resource selection, configuration — and why Cucumber's engine rather than Jupiter decides what runs. Expect a follow-up on what the engine emits per scenario.
An interviewer expects you to debug a suite that discovers nothing: missing engine artifact, features not copied to the test classpath, a selector written as a package, or a build that never enabled the JUnit Platform.
Own the convention across repositories: how many suite classes exist and what each one gates, whether feature files ship inside the test jar, and how the selector choice survives a module split or a monorepo migration.
## What the suite class actually is Cucumber-JVM does not bolt onto JUnit; it ships a **JUnit Platform test engine**. The platform's job is to ask every engine on the test classpath "what tests do you find here?", and Cucumber's engine — in the `cucumber-junit-platform-engine` artifact, registered under the engine id `cucumber` — answers by parsing `.feature` resources and returning one test per scenario. Jupiter answers the same question for `@Test` methods. They are peers. That leaves exactly one gap: something has to start a run and say *where to look*. On a plain Maven or Gradle build the suite class is that something: ``` @Suite @IncludeEngines("cucumber") @SelectClasspathResource("com/vinylmarket/acceptance") @ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "com.vinylmarket.acceptance.steps") public class RunCucumberTest { } ``` Four annotations, four separate jobs: - `@Suite` declares the class a JUnit Platform suite. It is executed by the platform's suite engine, so the suite engine artifact has to be on the test classpath too. - `@IncludeEngines("cucumber")` filters the suite's nested discovery down to the engine whose id is `cucumber`. It does not install anything — presence on the classpath does that — it decides who is invited. - `@SelectClasspathResource` is the selector: the slash-separated **resource directory** that holds the feature files. - `@ConfigurationParameter` carries Cucumber's own options into the run; the glue package is the one you almost always need. The class body stays empty. Adding a `@Test` method to it does nothing useful: that method belongs to Jupiter, and the engine filter excludes Jupiter from this suite anyway. ## The classpath detail that breaks most first attempts Feature files are **resources**, not sources. A Maven or Gradle build copies `src/test/resources` onto the test classpath and leaves non-Java files in `src/test/java` alone unless it is explicitly told otherwise. So a suite that discovers zero tests almost always means one of: 1. the features sit beside the step definitions in `src/test/java`; 2. the selector string was written as a package (`com.vinylmarket.acceptance`) instead of a resource path (`com/vinylmarket/acceptance`); 3. `cucumber-junit-platform-engine` is missing, so no engine claims the selectors; 4. the build was never told to use the JUnit Platform, so nothing runs the suite class at all. A useful habit is to mirror the package layout in `src/test/resources`, so the feature directory and the glue package read almost identically — but remember they are still two different strings pointing at two different things. ## Choosing a selector | Selector | What it names | When to use it | |---|---|---| | `@SelectClasspathResource` | a resource directory or single resource on the test classpath | the default; portable across modules and CI images | | `@SelectFile` | one file, by path | debugging a single feature | | `@SelectDirectories` | a filesystem directory relative to the working directory | features that genuinely live outside the classpath | | `@SelectPackages` | a package, resolved to its classpath location | mirrors the code layout when features ship in the test jar | Directory selection is the one that quietly bites: it resolves against the JVM's working directory, which is the module directory locally and often the repository root in CI. ## What the engine emits Granularity matters for everything downstream: - one platform test per **scenario**, named from the scenario name; - a `Scenario Outline` contributes one test per `Examples` row, not one for the outline; - feature files appear as containers, not as tests; - failures surface as ordinary test failures, so the build fails without extra wiring; - an IDE can re-run a single scenario, because a single scenario is a first-class platform test. That is the practical reason to prefer the platform suite over the command-line runner: the results arrive in the same shape as every other test in the build, and the report tooling you already have understands them. ## The older JUnit 4 route A class annotated `@RunWith(io.cucumber.junit.Cucumber.class)` with `io.cucumber.junit.CucumberOptions` is a different artifact (`cucumber-junit`) and a JUnit 4 runner rather than a platform engine. It still works through the vintage engine, and plenty of estates still run it, but it is not the platform suite: its options live in the annotation instead of in `cucumber.*` configuration keys, and it reports one JUnit 4 test per scenario through a runner rather than through engine discovery. Know both exist, name which one you mean, and do not mix the two annotation sets on the same class.
- Why does the Cucumber-JVM suite class contain no test methods?Because it is not a Jupiter test class. Discovery is done by the `cucumber` engine, which reads `.feature` resources and builds its own test descriptors; the suite class only carries selectors and configuration. A `@Test` method added to it would belong to Jupiter, and the engine filter keeps Jupiter out of this suite.
- What changes if you swap @SelectClasspathResource for @SelectDirectories?Directory selection resolves a filesystem path against the JVM's working directory. That works locally and then breaks in a multi-module build or a CI image where the working directory is the repository root. Classpath selection travels with the packaged test resources, so it is the portable default; use directory selection only when features live outside the classpath.
- The suite runs and reports zero tests. Where do you look first?Check that the feature files were copied onto the test classpath, that the selector is a slash-separated resource path rather than a package name, and that the engine artifact is a test dependency. Then confirm the build actually uses the JUnit Platform. Zero tests is a discovery problem, so nothing in the glue can explain it.
The suite class is a work order rather than a worker: it names the engine to call and the shelf of feature files to read, then steps out of the way.
saying these in an interview costs you the question
- Adds @Test methods to the suite class
- Puts .feature files in src/test/java and expects discovery
- Confuses the feature resource path with the glue package
- Thinks the JUnit 4 runner and the platform suite are one route
- Believes the engine creates one test per feature file