tessl install tessl/maven-io-cucumber--cucumber-expressions@19.0.0Cucumber Expressions are simple patterns for matching Step Definitions with Gherkin steps
This document covers corner cases, unusual inputs, and advanced scenarios that require special handling.
Expression expr = factory.createExpression("");
Optional<List<Argument<?>>> match1 = expr.match("");
// match1.isPresent() = true, arguments = [] (empty list)
Optional<List<Argument<?>>> match2 = expr.match("any text");
// match2.isPresent() = false// Transformer that handles null gracefully
Transformer<String> nullSafeTransformer = (String arg) -> {
return arg == null ? "default" : arg.trim();
};
ParameterType<String> nullableType = new ParameterType<>(
"nullable",
".*",
String.class,
nullSafeTransformer
);
// Transformer that returns null
Transformer<Optional<String>> optionalTransformer = (String arg) -> {
return (arg == null || arg.isEmpty()) ? null : arg;
};// String parameter with empty quotes
Expression expr = factory.createExpression("Value is {string}");
Optional<List<Argument<?>>> match = expr.match("Value is \"\"");
// match.isPresent() = true, value = "" (empty string)
// Anonymous parameter
Expression expr2 = factory.createExpression("Value is {}");
Optional<List<Argument<?>>> match2 = expr2.match("Value is ");
// match2.isPresent() = false (doesn't match trailing space alone)// Whitespace in expression is significant
Expression expr = factory.createExpression("I have {int} cucumbers");
expr.match("I have 42 cucumbers"); // ✓ Matches
expr.match("I have 42 cucumbers"); // ✗ Doesn't match (extra spaces)
expr.match("I have 42cucumbers"); // ✗ Doesn't match (missing space)// Tabs are not equivalent to spaces
Expression expr = factory.createExpression("Column\t{int}");
expr.match("Column\t42"); // ✓ Matches (tab character)
expr.match("Column 42"); // ✗ Doesn't match (space character)
// Newlines must be matched explicitly
Expression expr2 = factory.createExpression("Line1{string}Line2");
expr2.match("Line1\"text\nmore\"Line2"); // Depends on implementation// Use regex for flexible whitespace
Expression expr = factory.createExpression("^I\\s+have\\s+(\\d+)\\s+cucumbers$");
expr.match("I have 42 cucumbers"); // ✓ Matches
expr.match("I\thave\t42\tcucumbers"); // ✓ MatchesExpression expr = factory.createExpression("I have {int} cucumbers");
expr.match("I have 42 cucumbers"); // ✓ Matches
expr.match("I HAVE 42 CUCUMBERS"); // ✗ Doesn't match
expr.match("i have 42 cucumbers"); // ✗ Doesn't match// Case-insensitive color type
Transformer<Color> caseInsensitiveColor = (String s) -> {
return Color.valueOf(s.toUpperCase());
};
ParameterType<Color> colorType = new ParameterType<>(
"color",
"(?i)red|blue|green", // Case-insensitive regex
Color.class,
caseInsensitiveColor
);// Use (?i) flag for case-insensitive regex
Expression expr = factory.createExpression("^(?i)I have (\\d+) cucumbers$");
expr.match("I HAVE 42 CUCUMBERS"); // ✓ Matches
expr.match("i have 42 cucumbers"); // ✓ Matches// Unicode characters supported in expressions
Expression expr = factory.createExpression("I have {int} 🥒");
Optional<List<Argument<?>>> match = expr.match("I have 42 🥒");
// ✓ Matches
// Emoji in parameter type names (not recommended but works)
ParameterType<String> emojiType = new ParameterType<>(
"emoji",
"[\\p{So}]+",
String.class,
s -> s
);
Expression expr2 = factory.createExpression("Reaction is {emoji}");
Optional<List<Argument<?>>> match2 = expr2.match("Reaction is 😀🎉");
// ✓ Matches// Positions are based on Java char indices (UTF-16 code units)
String text = "I have 42 🥒"; // Emoji is 2 chars in UTF-16
Expression expr = factory.createExpression("I have {int} 🥒");
Optional<List<Argument<?>>> match = expr.match(text);
if (match.isPresent()) {
Group group = match.get().get(0).getGroup();
int start = group.getStart(); // Position in char indices
int end = group.getEnd();
// For proper Unicode handling with surrogate pairs:
int codePointStart = text.codePointCount(0, start);
}// Non-ASCII text in expressions and parameters
Expression expr = factory.createExpression("Usuario {word} tiene {int} años");
Optional<List<Argument<?>>> match = expr.match("Usuario José tiene 25 años");
// ✓ Matches
// Chinese characters
Expression expr2 = factory.createExpression("用户 {word} 有 {int} 个");
Optional<List<Argument<?>>> match2 = expr2.match("用户 张三 有 42 个");
// ✓ Matches// Anchors are literal text in Cucumber Expression mode
Expression expr = factory.createExpression("I have {int} cucumbers^");
expr.match("I have 42 cucumbers^"); // ✓ Matches (literal ^ at end)
expr.match("I have 42 cucumbers"); // ✗ Doesn't match (missing ^)
// To use regex anchors, must start with ^ or end with $
Expression regexExpr = factory.createExpression("^I have {int} cucumbers$");
// This creates a RegularExpression with proper anchors// Escape special characters with backslash
Expression expr = factory.createExpression("Price is {float} \\$");
expr.match("Price is 99.99 $"); // ✓ Matches
// Characters that need escaping: \ { } ( ) /
Expression expr2 = factory.createExpression("Value \\{type\\} is {int}");
expr2.match("Value {type} is 42"); // ✓ Matches// Dot is literal in Cucumber Expression mode
Expression expr = factory.createExpression("Version {int}.{int}.{int}");
expr.match("Version 1.2.3"); // ✓ Matches
expr.match("Version 1a2b3"); // ✗ Doesn't match (dots are required)
// In regex mode, dot matches any character
Expression regexExpr = factory.createExpression("^Version (\\d+).(\\d+).(\\d+)$");
regexExpr.match("Version 1a2b3"); // ✓ Matches (dots match any char)// English locale: comma as thousands separator
ParameterTypeRegistry englishRegistry = new ParameterTypeRegistry(Locale.ENGLISH);
ExpressionFactory englishFactory = new ExpressionFactory(englishRegistry);
Expression expr = englishFactory.createExpression("Value is {int}");
expr.match("Value is 1,234"); // ✓ Matches, parses as 1234
expr.match("Value is 1.234"); // ✗ Doesn't match (period not valid for int)
// French locale: period as thousands separator
ParameterTypeRegistry frenchRegistry = new ParameterTypeRegistry(Locale.FRENCH);
ExpressionFactory frenchFactory = new ExpressionFactory(frenchRegistry);
Expression exprFr = frenchFactory.createExpression("Valeur est {int}");
exprFr.match("Valeur est 1.234"); // ✓ Matches, parses as 1234
exprFr.match("Valeur est 1,234"); // ✗ Doesn't match (comma is decimal sep in French)Expression expr = factory.createExpression("Value is {double}");
expr.match("Value is 1.23e5"); // ✓ Matches, parses as 123000.0
expr.match("Value is 1.23E-5"); // ✓ Matches, parses as 0.0000123
expr.match("Value is 1.23e"); // ✗ Doesn't match (incomplete exponent)Expression expr = factory.createExpression("Value is {float}");
Optional<List<Argument<?>>> match = expr.match("Value is -0.0");
// ✓ Matches
if (match.isPresent()) {
Float value = (Float) match.get().get(0).getValue();
// value = -0.0 (negative zero is preserved)
System.out.println(1.0f / value); // Prints: -Infinity
}// Integer overflow
Expression intExpr = factory.createExpression("Value is {int}");
Optional<List<Argument<?>>> match1 = intExpr.match("Value is 2147483648");
// Throws NumberFormatException during getValue() (exceeds Integer.MAX_VALUE)
// Use long or BigInteger for large numbers
Expression longExpr = factory.createExpression("Value is {long}");
Optional<List<Argument<?>>> match2 = longExpr.match("Value is 2147483648");
// ✓ Matches
Expression bigIntExpr = factory.createExpression("Value is {biginteger}");
Optional<List<Argument<?>>> match3 = bigIntExpr.match("Value is 999999999999999999999");
// ✓ Matches// Parameter type with nested capture groups
ParameterType<Coordinate> coordType = new ParameterType<>(
"coord",
"((\\d+),(\\d+))", // Nested groups
Coordinate.class,
(String[] args) -> {
// args[0] = full match "10,20"
// args[1] = first capture "10"
// args[2] = second capture "20"
return new Coordinate(
Integer.parseInt(args[1]),
Integer.parseInt(args[2])
);
}
);// Non-capturing group (?:...) doesn't create argument
Expression expr = factory.createExpression("^(?:GET|POST) /api/(\\d+)$");
Optional<List<Argument<?>>> match = expr.match("GET /api/123");
if (match.isPresent()) {
// Only one argument from the (\\d+) group
Integer id = (Integer) match.get().get(0).getValue();
// No argument for (?:GET|POST) non-capturing group
}// Positive lookahead
Expression expr = factory.createExpression("^Value (\\d+)(?= units)$");
expr.match("Value 42 units"); // ✓ Matches, extracts 42
// Negative lookahead
Expression expr2 = factory.createExpression("^Value (\\d+)(?! units)$");
expr2.match("Value 42 items"); // ✓ Matches
expr2.match("Value 42 units"); // ✗ Doesn't match// UNSAFE: Modifying registry during concurrent use
ParameterTypeRegistry registry = new ParameterTypeRegistry(Locale.ENGLISH);
ExpressionFactory factory = new ExpressionFactory(registry);
// Thread 1: Using factory
new Thread(() -> {
Expression expr = factory.createExpression("I have {color} ball");
}).start();
// Thread 2: Modifying registry (RACE CONDITION!)
new Thread(() -> {
registry.defineParameterType(colorType); // ⚠️ NOT THREAD-SAFE
}).start();
// SAFE: Complete registration before concurrent use
registry.defineParameterType(colorType); // Register first
// Now safe to use concurrently// Expression instances are thread-safe
Expression expr = factory.createExpression("I have {int} cucumbers");
// Safe: Multiple threads matching same expression
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
final int count = i;
executor.submit(() -> {
String text = "I have " + count + " cucumbers";
Optional<List<Argument<?>>> match = expr.match(text);
// Thread-safe operation
});
}// Transformer that throws checked exception
Transformer<User> userTransformer = (String username) -> {
User user = database.findUser(username); // May throw SQLException
if (user == null) {
throw new UserNotFoundException(username);
}
return user;
};
// Exception is caught and re-thrown during getValue()
Expression expr = factory.createExpression("User {user} logs in");
Optional<List<Argument<?>>> match = expr.match("User john logs in");
if (match.isPresent()) {
try {
User user = (User) match.get().get(0).getValue();
} catch (RuntimeException e) {
Throwable cause = e.getCause();
if (cause instanceof UserNotFoundException) {
// Handle user not found
}
}
}// Multiple validation checks
Transformer<Email> emailTransformer = (String s) -> {
if (s == null || s.isEmpty()) {
throw new IllegalArgumentException("Email cannot be empty");
}
if (!s.contains("@")) {
throw new IllegalArgumentException("Email must contain @");
}
if (!s.matches("[\\w.+-]+@[\\w.-]+\\.[a-z]{2,}")) {
throw new IllegalArgumentException("Invalid email format: " + s);
}
String domain = s.substring(s.indexOf('@') + 1);
if (domain.equals("example.com")) {
throw new IllegalArgumentException("Example.com domain not allowed");
}
return new Email(s);
};// Two types with same pattern
ParameterType<Integer> type1 = new ParameterType<>(
"count", "\\d+", Integer.class, Integer::parseInt
);
ParameterType<String> type2 = new ParameterType<>(
"number", "\\d+", String.class, s -> s
);
registry.defineParameterType(type1);
registry.defineParameterType(type2);
try {
// Ambiguous in regex mode
Expression expr = factory.createExpression("^Value (\\d+)$");
// Throws: AmbiguousParameterTypeException
} catch (AmbiguousParameterTypeException e) {
// Resolve by using preferForRegexpMatch
ParameterType<Integer> preferred = new ParameterType<>(
"count", "\\d+", Integer.class, Integer::parseInt, true, true
);
}
// Not ambiguous in Cucumber Expression mode
Expression expr2 = factory.createExpression("Value {count}"); // Uses type1
Expression expr3 = factory.createExpression("Value {number}"); // Uses type2// Specific pattern should be registered first
ParameterType<Email> emailType = new ParameterType<>(
"email",
"[\\w.+-]+@[\\w.-]+\\.[a-z]{2,}",
String.class,
s -> s
);
ParameterType<String> wordType = new ParameterType<>(
"word",
"[\\w.+-]+", // Overlaps with email pattern
String.class,
s -> s
);
// Register more specific type first
registry.defineParameterType(emailType);
registry.defineParameterType(wordType);// Expression with many parameters
StringBuilder exprBuilder = new StringBuilder("Data:");
for (int i = 0; i < 100; i++) {
exprBuilder.append(" {int}");
}
Expression expr = factory.createExpression(exprBuilder.toString());
// Performance degrades with many parameters// Deeply nested optionals
Expression expr = factory.createExpression(
"I (have (seen (the (big (red (ball))))))"
);
// Complex pattern compilation and matching// Cache with thousands of expressions
ConcurrentHashMap<String, Expression> cache = new ConcurrentHashMap<>();
for (int i = 0; i < 10000; i++) {
String pattern = "Pattern " + i + " with {int}";
Expression expr = factory.createExpression(pattern);
cache.put(pattern, expr);
}
// Memory usage: ~10MB for 10,000 expressions