Choosing Cucumber Expressions over regex for readable, typed step definitions
Cucumber Expressions replace regex capture groups with readable placeholders and automatic type conversion. Custom parameter types let you define domain concepts once and reuse them across steps, while regex remains for patterns expressions cannot express.
11 Oct 2025, 06:49 UTC

Teams adopting Cucumber often start with regular expressions because they're familiar. A step like Given the user has (\d+) items priced at \$(\d+\.\d{2}) works, but the glue code then manually parses strings into integers and decimals. That parsing boilerplate clutters step definitions and fails at runtime with NumberFormatException when the pattern shifts. Cucumber Expressions, introduced in Cucumber 3.0 (2017), replace that friction with readable placeholders and automatic type conversion.
What changes when you switch to expressions
A Cucumber Expression is a plain string pattern such as Given the user has {int} items priced at {currency}. The tokens {int} and {currency} are matched by the expression engine, not by regex capture groups. When the step definition declares parameters as Integer and BigDecimal, the engine converts captured text to those types before invoking the method. No Integer.parseInt() or new BigDecimal() in your step code.
In Cucumber JVM (Java/Kotlin), any string pattern passed to @Given, @When, or @Then is interpreted as a Cucumber Expression. Regex remains available by passing a compiled Pattern object. In Cucumber.js, string patterns are expressions by default; regex requires an explicit RegExp object.
Domain-specific types eliminate repeated parsing logic
The real leverage comes from custom parameter types. You define a domain concept once — say, a currency amount or a user role — and reuse it across every feature file. The parser validates the format and hands your step definition a typed object.
In a JVM project using JUnit 5, register the type in a glue class loaded at startup:
// src/test/java/com/example/bdd/ParameterTypes.java
package com.example.bdd;
import io.cucumber.java.ParameterType;
import java.math.BigDecimal;
public class ParameterTypes {
@ParameterType(name = "currency", regexp = "\\$?\\d+(\\.\\d{2})?")
public BigDecimal currency(String input) {
return new BigDecimal(input.replace("$", ""));
}
}
The step definition then receives a BigDecimal directly:
// src/test/java/com/example/bdd/PriceSteps.java
package com.example.bdd;
import io.cucumber.java.en.Given;
import java.math.BigDecimal;
public class PriceSteps {
@Given("the price is {currency}")
public void the_price_is(BigDecimal price) {
// price is already a BigDecimal
}
}
In Cucumber.js (TypeScript), register the same concept in support code loaded before tests:
// support/parameter-types.ts
import { defineParameterType } from '@cucumber/cucumber';
import { BigDecimal } from 'big.js';
defineParameterType({
name: 'currency',
regexp: /\$?\d+(\.\d{2})?/,
transformer: (s: string) => new BigDecimal(s.replace('$', ''))
});
Feature files now read Given the price is $19.99 and the step receives a BigDecimal.
Running a quick verification
From the project root (read access to source, write access to build directory), run the test suite to confirm matching and type conversion:
- JVM:
mvn test -Dcucumber.filter.tags="@smoke" --plugin pretty - JS:
npm test -- --format=pretty
The pretty plugin output shows each step match and the resolved parameter type. Verify that the {int} step receives an Integer without manual parsing, and that your custom {currency} step receives a BigDecimal (or Big.js instance in JS).
Where expressions fall short
Cucumber Expressions cannot express alternation (|), optional groups (?), lookaheads, backreferences, or conditional capture. If a step genuinely needs Given the status is active or inactive with a single capture, model it as a custom parameter type {status} that maps active|inactive to an enum, rather than forcing regex alternation into the expression.
Type conversion failures surface as generic CucumberExpressionException messages. Unit-test your parameter type transformers in isolation with representative inputs (including malformed ones) so failures are diagnosable. Registration order matters: more specific types ({currency}) must be registered before general ones ({float}) to avoid greedy matching.
Parallel execution is safe because the parameter type registry is immutable after startup. However, mutable global state inside a transformer function (e.g., a cache that mutates) can cause flakiness — keep transformers pure.
Adopt incrementally, document both styles
Default to Cucumber Expressions for new step definitions. Migrate regex steps only when you touch them. Register domain parameter types in a central, version-controlled location (e.g., a ParameterTypes class or support/parameter-types.ts). Reserve regex for patterns expressions cannot express. This keeps feature files readable for product owners and step definitions type-safe for engineers.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.