Cucumber Expressions

Cucumber Expressions are simple patterns for matching Step Definitions with Gherkin steps.

Cucumber Expressions offer similar functionality to Regular Expressions, with the following improvements:

  • Improved readability
  • Custom parameter types
  • Expression generation

Introduction

Let's write a Cucumber Expression that matches the following text:

I have 42 cucumbers in my belly

The simplest Cucumber Expression that matches that text would be the text itself, but we can also write a slightly more generic one, with an int output parameter:

I have {int} cucumbers in my belly

When the text is matched against that expression, the number 42 is extracted from the {int} output parameter.

(Cucumber passes the extracted values as arguments to your Step Definitions).

The following text would not match the expression:

I have 42.5 cucumbers in my belly

This is because 42.5 does not look like an int (integers don't have a decimal part). Let's change the output parameter to float instead:

I have {float} cucumbers in my belly

Now the expression will match the text, and the float 42.5 is extracted.

Parameter types

The text between the curly braces is the name of a parameter type. The built-in parameter types are:

  • {int}, for example 71 or -19
  • {float}, for example 3.6, .8 or -9.2
  • {word}, for example banana (but not banana split)
  • {string}, for example "bangers" or 'mash'. The single/double quotes themselves are removed from the match.

On the JVM, there are additional parameter types for bigint, bigdecimal, byte, short, long and double.

Custom parameter types

You can define custom parameter types to represent types from your own domain. Doing this has the following benefits:

  1. Automatic conversion to custom types
  2. Document and evolve your ubiquitous domain language
  3. Enforce certain patterns

Imagine we want a parameter to match the colors red, blue or yellow (but nothing else). Let's assume a Color class is already defined. This is how we would define a custom color parameter type:

parameterTypeRegistry.defineParameterType(new ParameterType<>(
        "color",                                  // name
        "red|blue|yellow",                        // regexp
        Color.class,                              // type
        new SingleTransformer<Color>(Color::new), // transform
        false,                                    // useForSnippets
        false                                     // preferForRegexpMatch
));
parameterTypeRegistry.defineParameterType(
  new ParameterType(
    'color',           // name
    /red|blue|yellow/, // regexp
    Color,             // type
    s => new Color(s), // transform
    false,             // useForSnippets
    true               // preferForRegexpMatch
  )
)

The transform function can also return a Promise:

parameterTypeRegistry.defineParameterType(
  new ParameterType(
    'asyncColor',
    /red|blue|yellow/,
    Color,
    async s => new Color(s),
    false,
    true
  )
)
parameter_registry.define_parameter_type(ParameterType.new(
    'color',                   # name
    /red|blue|yellow/,         # regexp
    Color,                     # type
    lambda {|s| Color.new(s)}, # transform
    true,                      # use_for_snippets
    false                      # prefer_for_regexp_match
))

The parameters are as follows:

  • name - the name the parameter type will be recognised by in output parameters.
  • regexp - a regexp that will match the parameter. May include capture groups.
  • type
  • transform - a function that transforms the match from the regexp. Must have arity 1 if the regexp doesn't have any capture groups. Otherwise the arity must match the number of capture groups.
  • useForSnippets (Ruby: use_for_snippets) - Defaults to true. That means this parameter type will be used to generate snippets for undefined steps. If the regexp frequently matches text you don't intend to be used as arguments, disable its use for snippets with false.
  • preferForRegexpMatch (Ruby: prefer_for_regexp_match) - Defaults to false. Set to true if you use regular expressions and you want this parameter type's regexp take precedence over others during a match.

Now assume we have a Gherkin step with the following text following the step keyword:

I have a red ball

This will now be matched by following Cucumber Expression:

I have a {color} ball

Not only that, but the extracted argument will be of type Color.

The following text would not match the expression, because green isn't part of the parameter definition.

I have a green ball

Optional text

It's grammatically incorrect to say 1 cucumbers, so we should make that s optional. That can be done by surrounding the optional text with parenthesis:

I have {int} cucumber(s) in my belly

That expression would match this text:

I have 1 cucumber in my belly

It would also match this text:

I have 42 cucumbers in my belly

Alternative text

Sometimes you want to relax your language, to make it flow better. For example:

I have {int} cucumber(s) in my belly/stomach

This would match either of those texts:

I have 42 cucumbers in my belly
I have 42 cucumbers in my stomach

Step Definition Snippets (Cucumber Expression generation)

When Cucumber encounters a Gherkin step without a matching Step Definition, it will print a step Step Definition snippet with a matching Cucumber Expression that you can use as a starting point.

Consider this Gherkin step:

Given I have 3 red balls

If you had registered the color parameter type above, Cucumber would suggest a Step Definition with the following Cucumber Expression:

I have {int} {color} balls

If you hadn't registered the color parameter type, Cucumber would have suggested the following Cucumber Expression (only the built-in int parameter would be recognised):

I have {int} red balls

As you can see, Cucumber will generate better snippets for you if you define your own parameter types, and you'll also end up with a more consistent domain language in your Gherkin scenarios.

Regular Expressions

Cucumber has a long relationship with Regular Expressions, and they are still fully supported, just better.

The Cucumber Expression library's Regular Expression support passes capture groups through parameter types' transformers, just like Cucumber Expressions.

Imagine you have registered the Color parameter above, and have a step definition with the following Regular Expression:

I have a (red|blue|yellow) ball

The following Gherkin step would automatically convert to a Color instance:

I have a red ball

Built-in parameters for int and float also apply. The following Regular Expression would automatically convert arguments to int:

I have (\d+) cukes in my belly

Preferential parameter types

In some cases you might want to define two or more parameter types with the same regular expression, but different name and transform. For example, you may want to define name and person parameter types with the regexp [A-Z]+\w+, which would match Joe, Amy and so on.

If you are using a Regular Expressions like the following

([A-Z]+\w+) has invited ([A-Z]+\w+)

And try to match that against the following text:

Joe has invited Amy

Cucumber will not know whether to use the name or person parameter types, because they both match.

In that case you can switch to using Cucumber Expressions, where parameters are named, so there is no ambiguity:

{person} has invited {name}

Alternatively, you can continue to use Regular Expressions if you prefer, and make one of the parameter types preferential when you define it. A preferential parameter type will always be chosen over a non-preferential one.

When several parameter types share the same regular expression, only one of the parameter types can be preferential. If you define two parameter types with the same regular expression that are both preferential you will get an error during matching.

For contributors

If you're contributing to Cucumber, you might be interested in how to use Cucumber Expressions programmatically. Here are some pointers:

Capturing match arguments

When a Gherkin step is matched against an expression (CucumberExpression or RegularExpression), the result of the match is a list of Argument (or null/nil if there was no match).

This API is similar to most regexp APIs, but the Argument type has additional information:

  • value - the string value of the match against the expression.
  • value - the transformed value of the match (transformed by the parameter)
  • offset - the offset from the start of the text where the value was found

Arguments are captured by creating an expression, and invoking match with a string.

Capturing with CucumberExpression

String expr = "I have {int} cuke(s)";
Expression expression = new CucumberExpression(expr, parameterTypeRegistry);
List<Argument<?>> args = expression.match("I have 7 cukes");
assertEquals(7, args.get(0).getValue());
const expr = 'I have {int} cuke(s)'
const expression = new CucumberExpression(expr, parameterTypeRegistry)
const args = expression.match('I have 7 cukes')
assert.equal(7, args[0].value)
expr = "I have {int} cuke(s)"
expression = CucumberExpression.new(expr, parameter_registry)
args = expression.match("I have 7 cukes")
expect(args[0].value).to eq(7)

Capturing with RegularExpression

Pattern expr = Pattern.compile("I have (\\d+) cukes? in my (\\w+) now");
Expression expression = new RegularExpression(expr, parameterTypeRegistry);
List<Argument<?>> match = expression.match("I have 7 cukes in my belly now");
assertEquals(7, match.get(0).getValue());
assertEquals("belly", match.get(1).getValue());
const expr = /I have (\d+) cukes? in my (\w+) now/
const expression = new RegularExpression(expr, parameterRegistry)
const args = expression.match('I have 7 cukes in my belly now')
assert.equal(7, args[0].value)
assert.equal('belly', args[1].value)
expr = /I have (\d+) cukes? in my (\w*) now/
expression = RegularExpression.new(expr, parameter_type_registry)
args = expression.match("I have 7 cukes in my belly now")
expect( args[0].value ).to eq(7)
expect( args[1].value ).to eq("belly")

Generating snippets

When Cucumber can't find a matching step definition, it will generate a snippet of code. This snippet will have a suggested Cucumber Expression, and a function signature with the appropriate type(s).

This information is in a GeneratedExpression object, which is generated by a CucumberExpressionGenerator. The CucumberExpressionGenerator is aware of any registered parameters.

CucumberExpressionGenerator generator = new CucumberExpressionGenerator(parameterTypeRegistry);
String undefinedStepText = "I have 2 cucumbers and 1.5 tomato";
GeneratedExpression generatedExpression = generator.generateExpressions(undefinedStepText).get(0);
assertEquals("I have {int} cucumbers and {double} tomato", generatedExpression.getSource());
assertEquals(Double.class, generatedExpression.getParameterTypes().get(1).getType());
const generator = new CucumberExpressionGenerator(parameterRegistry)
const undefinedStepText = 'I have 2 cucumbers and 1.5 tomato'
const generatedExpression = generator.generateExpressions(
  undefinedStepText
)[0]
assert.equal(
  generatedExpression.source,
  'I have {int} cucumbers and {float} tomato'
)
assert.equal(generatedExpression.parameterNames[0], 'int')
assert.equal(generatedExpression.parameterTypes[1].name, 'float')
generator = CucumberExpressionGenerator.new(parameter_registry)
undefined_step_text = "I have 2 cucumbers and 1.5 tomato"
generated_expression = generator.generate_expression(undefined_step_text)
expect(generated_expression.source).to eq("I have {int} cucumbers and {float} tomato")
expect(generated_expression.parameter_types[1].type).to eq(Float)

Acknowledgements

The Cucumber Expression syntax is inspired by similar expression syntaxes in other BDD tools, such as Turnip, Behat and Behave.

Big thanks to Jonas Nicklas, Konstantin Kudryashov and Jens Engel for implementing those libraries.

results matching ""

    No results matching ""