tessl install tessl/maven-io-cucumber--cucumber-expressions@19.0.0Cucumber Expressions are simple patterns for matching Step Definitions with Gherkin steps
This guide covers frequently used patterns when working with Cucumber Expressions.
Match step definitions in BDD test frameworks.
import io.cucumber.cucumberexpressions.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public class StepDefinitions {
private final ExpressionFactory factory;
private final Map<String, Expression> expressionCache = new ConcurrentHashMap<>();
public StepDefinitions() {
ParameterTypeRegistry registry = new ParameterTypeRegistry(Locale.ENGLISH);
// Register custom types
registry.defineParameterType(new ParameterType<>(
"user", "[A-Za-z][A-Za-z0-9_]*", String.class, s -> s
));
this.factory = new ExpressionFactory(registry);
}
public Optional<List<Argument<?>>> matchStep(String pattern, String text) {
Expression expr = expressionCache.computeIfAbsent(
pattern,
factory::createExpression
);
return expr.match(text);
}
public Object[] extractArguments(String pattern, String text) {
Optional<List<Argument<?>>> match = matchStep(pattern, text);
if (match.isEmpty()) {
return null;
}
return match.get().stream()
.map(Argument::getValue)
.toArray();
}
}
// Usage
StepDefinitions steps = new StepDefinitions();
Object[] args = steps.extractArguments(
"User {user} logs in with {string}",
"User john logs in with \"password123\""
);
// args = ["john", "password123"]Register domain-specific types for your application.
public class DomainTypes {
public static void registerTypes(ParameterTypeRegistry registry) {
// Email type
registry.defineParameterType(new ParameterType<>(
"email",
"[\\w.+-]+@[\\w.-]+\\.[a-z]{2,}",
String.class,
s -> s
));
// URL type
registry.defineParameterType(new ParameterType<>(
"url",
"https?://[\\w.-]+(?:/[\\w.-]*)*",
String.class,
s -> s
));
// Date type
registry.defineParameterType(new ParameterType<>(
"date",
"\\d{4}-\\d{2}-\\d{2}",
java.time.LocalDate.class,
java.time.LocalDate::parse
));
// Enum types
registry.defineParameterType(ParameterType.fromEnum(Status.class));
registry.defineParameterType(ParameterType.fromEnum(Priority.class));
}
}
// Usage
ParameterTypeRegistry registry = new ParameterTypeRegistry(Locale.ENGLISH);
DomainTypes.registerTypes(registry);
ExpressionFactory factory = new ExpressionFactory(registry);
Expression expr = factory.createExpression("Send email to {email} on {date}");
Optional<List<Argument<?>>> match = expr.match("Send email to user@example.com on 2026-01-30");Handle errors gracefully with automatic recovery.
public class SafeExpressionMatcher {
private final ExpressionFactory factory;
public SafeExpressionMatcher(ExpressionFactory factory) {
this.factory = factory;
}
public MatchResult match(String pattern, String text) {
try {
Expression expr = factory.createExpression(pattern);
Optional<List<Argument<?>>> match = expr.match(text);
if (match.isPresent()) {
return MatchResult.success(match.get());
} else {
return MatchResult.noMatch();
}
} catch (UndefinedParameterTypeException e) {
return MatchResult.error(
"Undefined type: " + e.getUndefinedParameterTypeName()
);
} catch (AmbiguousParameterTypeException e) {
return MatchResult.error(
"Ambiguous types: " + e.getParameterTypes()
);
} catch (CucumberExpressionException e) {
return MatchResult.error("Expression error: " + e.getMessage());
}
}
public static class MatchResult {
private final List<Argument<?>> arguments;
private final String error;
private final Status status;
enum Status { SUCCESS, NO_MATCH, ERROR }
private MatchResult(List<Argument<?>> arguments, String error, Status status) {
this.arguments = arguments;
this.error = error;
this.status = status;
}
public static MatchResult success(List<Argument<?>> args) {
return new MatchResult(args, null, Status.SUCCESS);
}
public static MatchResult noMatch() {
return new MatchResult(null, null, Status.NO_MATCH);
}
public static MatchResult error(String error) {
return new MatchResult(null, error, Status.ERROR);
}
public boolean isSuccess() { return status == Status.SUCCESS; }
public boolean isNoMatch() { return status == Status.NO_MATCH; }
public boolean isError() { return status == Status.ERROR; }
public List<Argument<?>> getArguments() { return arguments; }
public String getError() { return error; }
}
}
// Usage
SafeExpressionMatcher matcher = new SafeExpressionMatcher(factory);
SafeExpressionMatcher.MatchResult result = matcher.match(
"I have {int} cucumbers",
"I have 42 cucumbers"
);
if (result.isSuccess()) {
Integer count = (Integer) result.getArguments().get(0).getValue();
System.out.println("Count: " + count);
} else if (result.isError()) {
System.err.println("Error: " + result.getError());
}Cache expressions for better performance.
public class CachedExpressionFactory {
private final ExpressionFactory factory;
private final Map<String, Expression> cache = new ConcurrentHashMap<>();
private final Map<String, Exception> errorCache = new ConcurrentHashMap<>();
public CachedExpressionFactory(ParameterTypeRegistry registry) {
this.factory = new ExpressionFactory(registry);
}
public Expression getOrCreateExpression(String expressionString) {
// Check error cache first
Exception cachedError = errorCache.get(expressionString);
if (cachedError != null) {
throw new RuntimeException("Previously failed to create expression", cachedError);
}
// Get or create expression
return cache.computeIfAbsent(expressionString, key -> {
try {
return factory.createExpression(key);
} catch (Exception e) {
errorCache.put(key, e);
throw e;
}
});
}
public void clearCache() {
cache.clear();
errorCache.clear();
}
public int getCacheSize() {
return cache.size();
}
public Map<String, Expression> getCachedExpressions() {
return Collections.unmodifiableMap(cache);
}
}
// Usage
CachedExpressionFactory cache = new CachedExpressionFactory(registry);
// First call: creates and caches expression
Expression expr1 = cache.getOrCreateExpression("I have {int} cucumbers");
// Second call: returns cached expression (fast!)
Expression expr2 = cache.getOrCreateExpression("I have {int} cucumbers");
assert expr1 == expr2; // Same instanceExtract arguments with type safety and validation.
public class ArgumentExtractor {
public static <T> T getValue(Optional<List<Argument<?>>> match,
int index,
Class<T> expectedType) {
if (match.isEmpty()) {
throw new IllegalArgumentException("No match found");
}
List<Argument<?>> args = match.get();
if (index >= args.size()) {
throw new IndexOutOfBoundsException(
"Argument index " + index + " out of range (size: " + args.size() + ")"
);
}
Object value = args.get(index).getValue();
if (value == null) {
return null;
}
if (!expectedType.isInstance(value)) {
throw new ClassCastException(
"Expected " + expectedType.getName() +
" but got " + value.getClass().getName()
);
}
return expectedType.cast(value);
}
public static <T> List<T> getValues(Optional<List<Argument<?>>> match,
Class<T> expectedType) {
if (match.isEmpty()) {
return Collections.emptyList();
}
return match.get().stream()
.map(arg -> {
Object value = arg.getValue();
if (value == null) {
return null;
}
if (!expectedType.isInstance(value)) {
throw new ClassCastException(
"Expected " + expectedType.getName() +
" but got " + value.getClass().getName()
);
}
return expectedType.cast(value);
})
.collect(Collectors.toList());
}
}
// Usage
Expression expr = factory.createExpression("User {string} has {int} items");
Optional<List<Argument<?>>> match = expr.match("User \"John\" has 5 items");
String username = ArgumentExtractor.getValue(match, 0, String.class);
Integer itemCount = ArgumentExtractor.getValue(match, 1, Integer.class);Validate data during transformation.
public class ValidatedTypes {
public static void registerTypes(ParameterTypeRegistry registry) {
// Age with validation
registry.defineParameterType(new ParameterType<>(
"age",
"\\d+",
Integer.class,
(String s) -> {
int value = Integer.parseInt(s);
if (value < 0 || value > 150) {
throw new IllegalArgumentException(
"Age must be between 0 and 150, got: " + value
);
}
return value;
}
));
// Email with validation
registry.defineParameterType(new ParameterType<>(
"email",
"[\\w.+-]+@[\\w.-]+\\.[a-z]{2,}",
String.class,
(String s) -> {
if (!s.contains("@")) {
throw new IllegalArgumentException("Invalid email: " + s);
}
return s;
}
));
// Positive number
registry.defineParameterType(new ParameterType<>(
"positive",
"\\d+",
Integer.class,
(String s) -> {
int value = Integer.parseInt(s);
if (value <= 0) {
throw new IllegalArgumentException(
"Must be positive, got: " + value
);
}
return value;
}
));
}
}
// Usage
ValidatedTypes.registerTypes(registry);
Expression expr = factory.createExpression("Person is {age} years old");
// Valid age
Optional<List<Argument<?>>> match1 = expr.match("Person is 25 years old");
Integer age1 = (Integer) match1.get().get(0).getValue(); // 25
// Invalid age - throws during getValue()
Optional<List<Argument<?>>> match2 = expr.match("Person is 200 years old");
try {
Integer age2 = (Integer) match2.get().get(0).getValue();
} catch (IllegalArgumentException e) {
System.err.println("Validation error: " + e.getMessage());
}Handle parameters with multiple capture groups.
// Define a Point type with x,y coordinates
class Point {
final int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
}
// Register parameter type with multiple captures
ParameterType<Point> pointType = new ParameterType<>(
"point",
"(\\d+),(\\d+)", // Two capture groups
Point.class,
(String[] args) -> new Point(
Integer.parseInt(args[0]),
Integer.parseInt(args[1])
)
);
registry.defineParameterType(pointType);
// Use in expression
Expression expr = factory.createExpression("Move cursor to {point}");
Optional<List<Argument<?>>> match = expr.match("Move cursor to 10,20");
if (match.isPresent()) {
Point point = (Point) match.get().get(0).getValue();
System.out.println("X: " + point.x + ", Y: " + point.y);
}Handle anonymous {} parameters with custom logic.
// Set default transformer
registry.setDefaultParameterTransformer((fromValue, toValueType) -> {
if (fromValue == null) return null;
// Handle UUID
if (toValueType == java.util.UUID.class) {
return java.util.UUID.fromString(fromValue);
}
// Handle LocalDate
if (toValueType == java.time.LocalDate.class) {
return java.time.LocalDate.parse(fromValue);
}
// Handle custom types
if (toValueType == CustomType.class) {
return CustomType.parse(fromValue);
}
// Fallback
return fromValue;
});
// Use anonymous parameters with type hints
Expression expr = factory.createExpression("ID is {}");
Optional<List<Argument<?>>> match = expr.match(
"ID is 550e8400-e29b-41d4-a716-446655440000",
java.util.UUID.class // Type hint
);
java.util.UUID uuid = (java.util.UUID) match.get().get(0).getValue();Generate expressions from example text.
public class SnippetGenerator {
private final CucumberExpressionGenerator generator;
public SnippetGenerator(ParameterTypeRegistry registry) {
this.generator = new CucumberExpressionGenerator(registry);
}
public String generateJavaSnippet(String stepText, String annotation) {
List<GeneratedExpression> expressions = generator.generateExpressions(stepText);
if (expressions.isEmpty()) {
return "// Could not generate expression for: " + stepText;
}
GeneratedExpression expr = expressions.get(0); // Most specific
StringBuilder snippet = new StringBuilder();
snippet.append("@").append(annotation).append("(\"")
.append(expr.getSource()).append("\")\n");
snippet.append("public void step(");
List<String> paramNames = expr.getParameterNames();
List<ParameterType<?>> paramTypes = expr.getParameterTypes();
for (int i = 0; i < paramNames.size(); i++) {
if (i > 0) snippet.append(", ");
snippet.append(paramTypes.get(i).getType().getTypeName())
.append(" ").append(paramNames.get(i));
}
snippet.append(") {\n");
snippet.append(" // TODO: implement\n");
snippet.append("}\n");
return snippet.toString();
}
}
// Usage
SnippetGenerator generator = new SnippetGenerator(registry);
String snippet = generator.generateJavaSnippet(
"I have 42 cucumbers in my belly",
"Given"
);
System.out.println(snippet);
// Output:
// @Given("I have {int} cucumbers in my belly")
// public void step(java.lang.Integer int1) {
// // TODO: implement
// }Register types only if not already present.
public class ConditionalTypeRegistry {
private final ParameterTypeRegistry registry;
private final Set<String> registeredTypes = new HashSet<>();
public ConditionalTypeRegistry(ParameterTypeRegistry registry) {
this.registry = registry;
}
public void registerIfNotExists(ParameterType<?> parameterType) {
String name = parameterType.getName();
if (registeredTypes.contains(name)) {
return; // Already registered
}
try {
registry.defineParameterType(parameterType);
registeredTypes.add(name);
} catch (DuplicateTypeNameException e) {
// Already registered by another source
registeredTypes.add(name);
}
}
public boolean isRegistered(String typeName) {
return registeredTypes.contains(typeName);
}
}
// Usage
ConditionalTypeRegistry conditionalRegistry = new ConditionalTypeRegistry(registry);
// Register types conditionally
conditionalRegistry.registerIfNotExists(colorType);
conditionalRegistry.registerIfNotExists(sizeType);
// Safe to call multiple times
conditionalRegistry.registerIfNotExists(colorType); // No-op