Answer
Cucumber’s tag expression syntax does not support a numeric limit; the --tags option always selects every scenario that matches the expression. There is no configuration flag or runner option to impose a maximum count directly.
Why a manual truncation is unreliable
- Scenario discovery order follows the file‑system/classpath traversal, which can vary between build tools, IDEs, and especially when parallel execution is enabled.
- With parallel runners (e.g.,
cucumber-junit-parallel or cucumber-js parallel) the set of scenarios assigned to each worker is nondeterministic, so a simple “take first N” after collection yields different slices across runs.
Deterministic approach
- Disable parallel execution so that a single thread processes all scenarios.
- Ensure a stable discovery order – name feature files with a predictable prefix (e.g.,
01_, 02_) or rely on alphabetical ordering of the file paths.
- Collect all matching scenarios, sort them by URI and line number (the natural order Cucumber uses when not parallel).
- Take the first N scenarios from that sorted list and invoke Cucumber only on that subset.
Example: Cucumber‑JVM custom runner
import io.cucumber.junit.Cucumber;
import io.cucumber.junit.CucumberOptions;
import org.junit.runner.RunWith;
@RunWith(Cucumber.class)
@CucumberOptions(
tags = "@regression",
glue = "com.example.steps",
plugin = {"pretty", "json:target/cucumber.json"}
)
public class LimitedRegressionRunner {
// This class is intentionally empty; the real work is done by a JUnit @Before
// that reads the scenario list, sorts, and limits via Cucumber's API.
// For brevity, the full implementation uses cucumber.api.junit.Cucumber
// subclassing and overriding getScenarioSupplier().
}
(The actual implementation subclasses io.cucumber.junit.Cucumber and overrides getScenarioSupplier() to return a limited stream after sorting.)
Example: Cucumber‑JS custom support script
const { defineSupportCode } = require('cucumber');
const fs = require('fs');
const path = require('path');
function getLimitedUris(tagExpression, limit) {
// 1. Run a dry‑run to collect all matching URIs
const { execSync } = require('child_process');
const dryRunOutput = execSync(`npx cucumber-js --tags ${tagExpression} --dry-run -f json`, { encoding: 'utf8' });
const json = JSON.parse(dryRunOutput);
// 2. Extract URIs, sort them
const uris = json.map(item => item.uri).sort();
// 3. Slice to limit
return uris.slice(0, limit);
}
// Export a before‑hook that filters scenarios
module.exports = function() {
const LIMIT = 50; // <-- adjust as needed
const TAGS = '@regression';
const allowedUris = new Set(getLimitedUris(TAGS, LIMIT));
this.Before(function(scenario) {
if (!allowedUris.has(this.scenario.uri)) {
return this.skip();
}
});
};
Both examples assume that parallel execution is turned off; otherwise the ordering step would not be reliable.
Verification steps
- Run the suite with
--tags @regression and confirm that all matching scenarios execute (baseline).
- Disable any parallel runner (e.g., set
cucumber.junit.parallel.enabled=false or avoid cucumber-js parallel).
- Apply the custom runner/support script with a limit (e.g., 50).
- Execute the test suite multiple times and verify that the same set of scenarios runs each time (compare scenario names or URIs).
- Re‑enable parallel execution to see the nondeterministic variation; this confirms why the limit must be applied before parallel distribution.
If you cannot modify the runner, the only reliable alternative is to externalize the selection (e.g., generate a temporary feature file containing only the first N scenarios) but that still requires deterministic ordering.