A Cucumber-JVM extension built on Cucumber-Guice. It adds scenario-scoped variables, SpEL templating, fluent assertion steps, and Cucumber parameter-type integration that resolves placeholders inside step args, doc strings, and data tables.
- Scenario-scoped variables — set/read from Gherkin, Java, or files; share between steps within one scenario
- File-based loading —
.properties/.yaml/.json/.xml/.txt/.csv/.html/.text - JSON Pointer access —
#[var/path/to/value]reaches into JSON-typed variables - SpEL templating —
#{ … }evaluates Spring Expression Language inside Gherkin and resource files - Predefined steps —
[util] Match …,[time-util] Check period …,[util] Wait …s - Transparent placeholder resolution —
#[var]and#{spel}are auto-resolved in step args, doc strings, and data tables - Built-in object matching — assertion steps delegate to JTest-Utils
ObjectMatcher
<dependency>
<groupId>io.github.fslev</groupId>
<artifactId>cucumber-jutils</artifactId>
<version>${latest.version}</version>
</dependency>The library declares Cucumber, Cucumber-Guice, and Guice as provided, so your project must pull them in:
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-java</artifactId>
<version>${cucumber.version}</version>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-guice</artifactId>
<version>${cucumber.version}</version>
</dependency>
<dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
<version>${guice.version}</version>
</dependency>Add com.cucumber.utils to your Cucumber glue path so that the predefined steps and parameter-type transformer are picked up:
@CucumberOptions(features = "src/test/resources/features",
glue = {"com.cucumber.utils", "your.own.steps"})- Java 17+
- Cucumber-JVM 6.10.0+
- Guice 4.2.1+
Scenario-scoped key/value store. Set from Gherkin or Java; read with #[name] in any step argument, doc string, or data-table cell. Lifetime is one scenario.
#[name] reads a variable. Steps that accept variable substitution must use Cucumber anonymous parameter types ({}).
Scenario: Set and read scenario variables
* var animal="rabbit"
* var location="forest"
* [util] Match some rabbit with some #[animal]
* [util] Match forest with #[location]Scenario: Variable from doc string
* var animal=
"""
rabbit
"""
* [util] Match some rabbit with some #[animal]Scenario: Variable from file
* var animal from file "features/readme/vars/madagascar.crt"
* [util] Match macac with #[animal]Scenario: Variable from data table
* var animals from table
| feline | marsupial |
| lioness | kangaroo |
| cougar | tasmanian devil |
* [util] Match [{"feline":"lioness", "marsupial":"kangaroo"}, {"feline":"cougar", "marsupial":"tasmanian devil"}] with #[animals]Inject ScenarioVars. Variables set in Java are visible from Gherkin and vice versa:
@ScenarioScoped
public class ScenarioVarsReadmeSteps {
@Inject
private ScenarioVars scenarioVars;
@Given("Some random step which sets some variables")
public void setVariables() {
scenarioVars.put("animal", "Cheetah");
Map<String, Object> vars = new HashMap<>();
vars.put("figure", "triangle");
vars.put("number", 10);
scenarioVars.putAll(vars);
}
}
@ScenarioScoped
public class ScenarioVarsAnotherReadmeSteps {
@Inject
private ScenarioVars scenarioVars;
@Given("Some random step which reads the variables")
public void readVariables() {
assertEquals("Cheetah", scenarioVars.getAsString("animal"));
assertEquals("triangle", scenarioVars.getAsString("figure"));
assertEquals(10, scenarioVars.get("number"));
}
}load vars from file parses .properties, .yaml, and .yml and stores each entry as a scenario variable.
Scenario: Load variables from properties file
* load vars from file "features/readme/vars/config.properties"
* [util] Match lioness with #[animal]
* [util] Match Africa with #[location]load vars from dir walks a directory recursively. Each .properties/.yaml/.yml is flattened into one variable per key; each .txt/.json/.xml/.csv/.html/.text becomes a single variable named after the file (without extension).
Given the directory:
placeholders/properties/drinks/
├── drink.yaml # beer: Bergenbier
│ # beers:
│ # - Ursus
│ # - Heineken
└── whisky.txt # Johnny Walker
Scenario: Load variables from directory tree
* load vars from dir "placeholders/properties/drinks"
* [util] Match Johnny Walker with #[whisky]
* [util] Match Bergenbier with #[beer]
* [util] Match ["Ursus", "Heineken"] with #[beers]whisky.txt becomes #[whisky] (filename without extension); drink.yaml is flattened so that beer and beers become top-level scenario variables.
@Inject
private ScenarioVars scenarioVars;
@Given("Read scenario variables from file")
public void setVariablesFromFile() {
ScenarioVarsUtils.loadScenarioVarsFromFile("features/readme/vars/config.properties", scenarioVars);
ScenarioVarsUtils.loadScenarioVarsFromDir("placeholders/properties/drinks", scenarioVars);
assertEquals("Africa", scenarioVars.get("location"));
assertEquals("Johnny Walker", scenarioVars.get("whisky"));
}ScenarioVarsUtils.parse reads a file and returns it with both #[var] placeholders and #{spel} expressions resolved. The variables it resolves are whatever has been put into ScenarioVars earlier in the scenario — set from Gherkin, from another step, or loaded from a file. Use this when you want the substituted string content (rather than registering each entry as its own variable, like loadScenarioVarsFromFile).
File:
features/readme/scene/some_text.txtThe #[animal] lives in #[location]
Scenario: Parse files for scenario variables
* var animal="wolf"
* var location="forest"
* Parse file for scenario variables@Inject
private ScenarioVars scenarioVars;
@Given("Parse file for scenario variables")
public void parseFileForScenarioVars() {
// animal="wolf" and location="forest" were set in the Gherkin steps above,
// so #[animal] resolves to "wolf" and #[location] resolves to "forest".
assertEquals("The wolf lives in forest",
ScenarioVarsUtils.parse("features/readme/scene/some_text.txt", scenarioVars));
}ScenarioVars.get(name) recognises four reserved keys that compute a value at read time:
| Name | Returns |
|---|---|
uid |
String — random UUID |
now |
Long — System.currentTimeMillis() |
short-random |
Integer — 0..Short.MAX_VALUE |
int-random |
Integer — 0..Integer.MAX_VALUE |
* var requestId="#[uid]"
* var ts="#[now]"For variables holding JSON, address inner values with / paths (Jackson JSON Pointer style):
Scenario: Path variables
* var x=
"""json
{"book":{"details":{"title":"Moby Dick"}}}
"""
* [util] Match #[x/book/details/title] with Moby DickWorks for both String-typed JSON values and Map/List values:
ScenarioVars vars = new ScenarioVars();
vars.put("var1", Map.of("a1", true, "a2", Map.of("a21", List.of("value1", 2, true))));
assertTrue(vars.get("var1/a2") instanceof ObjectNode);
assertEquals(new IntNode(2), vars.get("var1/a2/a21/1"));#{ … } evaluates Spring Expression Language. The pipeline is two-pass: #[var] substitution runs first, then #{spel} evaluation runs over the result. SpEL is therefore free to reference scenario variables.
Scenario: SpEL inside Gherkin
* var number="5"
* var isOddNumber="#{ #[number] % 2 != 0 }"
* [util] Match true with #[isOddNumber]If a file contains both placeholders and SpEL, ScenarioVarsUtils.parse (used internally by var content from file) resolves both:
File:
features/readme/scene/some_text_with_spel.txt"Is #[number] odd: #{ #[number] % 2 != 0 }"
Scenario: SpEL inside files
* var content from file "features/readme/scene/some_text_with_spel.txt"
* var number="5"
* [util] Match "Is 5 odd: true" with #[content]Steps shipped with the library — assertion, date/time arithmetic, and sleep — accessible to any feature once the com.cucumber.utils glue is configured.
# Equality (compares JSON, XML, regex, primitives — all via JTest-Utils ObjectMatcher)
* [util] Match true with true
* [util] Match {"a":1} with {"a":1, "b":2} using matchConditions=JSON_NON_EXTENSIBLE_OBJECT
* [util] Match expected against
"""
some doc-string content
"""
* [util] Match expected against file "path/to/file.json"
* [util] Match expected against table
| k1 | k2 |
| v1 | v2 |
* [util] Match someValue against NULL
# Inverse — assertion fails if the values match
* [util] Negative match a with bScenario: Date arithmetic and period checks
* var currentMillis="#[now]"
* [time-util] date var currentDate=from millis #[currentMillis] PLUS 0 YEARS with format pattern=yyyy-MM-dd
* [time-util] date var futureDate=from millis #[currentMillis] PLUS 15 YEARS with format pattern=yyyy-MM-dd
* [time-util] Check period from #[currentDate] to #[futureDate] is 15 YEARS using date pattern yyyy-MM-dd
* [time-util] Check period from 2019-02-03 23:58:12+0200 to 2019-02-04 01:59:10+0300 is 1 HOURS using date time pattern yyyy-MM-dd HH:mm:ssZ
* [time-util] date millis var ts=from date 2021-03-07 18:44:27.345+0000 PLUS 1 HOURS with format pattern=yyyy-MM-dd HH:mm:ss.SSSZ
* [util] Match #[ts] with 1615146267345Negative variants (doesn't match) and MINUS are supported:
* [time-util] Check period from 2020-02-03 to 2021-02-03 doesn't match 36 DAYS using date pattern yyyy-MM-dd
* [time-util] date var pastDate=from millis #[currentMillis] MINUS 31 DAYS with format pattern=yyyy-MM-dd* [util] Wait 10.471s
* [util] Wait 2.5mcom.cucumber.utils.context.config.ParameterTypesConfig registers a default Cucumber parameter transformer that runs every step argument, doc string, and data table cell through ScenarioVarsParser — first #[var] substitution, then #{spel} evaluation. After substitution, if the parameter type is not Object, the value is JSON-deserialized via Jackson, falling back to reflective conversion when it isn't valid JSON.
Two reserved literals are recognised in step args:
| Literal | Becomes |
|---|---|
[_null] |
null |
[_blank] |
"" (empty string) |
| Class | What you call |
|---|---|
ScenarioVars |
get, getAsString, put, putAll, nameSet, containsVariable, size |
ScenarioVarsUtils |
parse(filePath, vars), loadScenarioVarsFromFile, loadScenarioVarsFromDir, loadFileAsScenarioVariable |
ScenarioVarsParser |
parse(source, vars) — runs substitution + SpEL on a string |
ScenarioUtils |
log(msg, args...), getScenario() — write to the active Cucumber scenario report |
@Inject
private ScenarioUtils scenarioUtils;
@Given("write {}=\"{}\"")
public void writeSomething(String name, Object value) {
scenarioUtils.log("var {} = {}", name, value);
scenarioUtils.log("Scenario: {}", scenarioUtils.getScenario().getName());
}The Cucumber JUtils Tutorial walks through a full project setup.