Demo

In General

Cucumber expressions

Cucumber uses expressions to link a Gherkin Step to a Step Definition. You can use Regular Expressions or Cucumber Expressions.

Cucumber Expressions offer similar functionality to Regular Expressions, with a syntax that is more human to read and write. Cucumber Expressions are also extensible with parameter types.


Introduction

Let’s write a Cucumber Expression that matches the following Gherkin step (the Given keyword has been removed here, as it’s not part of the match).

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 more generic expression, 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 and passed as an argument to the step definition.

The following text would not match the expression:

I have 42.5 cucumbers in my belly 

This is because 42.5 has a decimal part, and doesn’t fit into an int. 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

Text between curly braces reference a parameter type. Cucumber comes with the following built-in parameter types:

Parameter TypeDescription
{int}Matches integers, for example 71 or -19.
{float}Matches floats, for example 3.6, .8 or -9.2.
{word}Matches words without whitespace, for example banana (but not banana split)
{string}Matches single-quoted or double-quoted strings, for example "banana split" or 'banana split' (but not banana split). Only the text between the quotes will be extracted. The quotes themselves are discarded. Empty pairs of quotes are valid and will be matched and passed to step code as empty strings.
{} anonymousMatches anything (/.*/).

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

The anonymous parameter type will be converted to the parameter type of the step definition using an object mapper. Cucumber comes with a built-in object mapper that can handle most basic types. Aside from Enum it supports conversion to BigInteger, BigDecimal, Byte, Short, Integer, Long, Float, Double and String.

To automatically convert to other types it is recommended to install an object mapper. See configuration to learn how.


Custom Parameter types

Cucumber Expressions can be extended so they automatically convert output parameters to your own types. Consider this Cucumber Expression:

I have a {color} ball 

If we want the {color} output parameter to be converted to a Color object, we can define a custom parameter type in Cucumber’s configuration.

typeRegistry.defineParameterType(new ParameterType<>(     
    "color",           // name     
    "red|blue|yellow", // regexp     
    Color.class,       // type     
    Color::new         // transformer function ))

The table below explains the various arguments you can pass when defining a parameter type.

ArgumentDescription
nameThe name the parameter type will be recognised by in output parameters.
regexpA regexp that will match the parameter. May include capture groups.
typeThe return type of the transformer method.
transformerA method 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 in regexp.
useForSnippetsDefaults 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.
preferFor
RegexpMatch
Defaults to false. Set to true if you have step definitions that use regular expressions, and you want this parameter type to take precedence over others during a match.

Optional text

It’s grammatically incorrect to say 1 cucumbers, so we should make the plural 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 

In Regular Expressions, parenthesis means capture group, but in Cucumber Expressions it means optional text.


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 

Alternative text only works when there is no whitespace between the alternative parts.


Escaping

If you ever need to match () or {} literally, you can escape the opening( or { with a backslash:

I have 42 \{what} cucumber(s) in my belly \(amazing!) 

This expression would match the following examples:

I have 42 {what} cucumber in my belly (amazing!) 
I have 42 {what} cucumbers in my belly (amazing!) 

You may have to escape the \ character itself with another \, depending on your programming language. For example, in Java, you have to use escape character \ with another backslash.

I have 42 \\{what} cucumber(s) in my belly \\(amazing!) 

Then this expression would match the following example:

I have 42 {what} cucumber in my belly (amazing!) 
I have 42 {what} cucumbers in my belly (amazing!) 

There is currently no way to escape a /character – it will always be interpreted as alternative text.

The original version of the guide can be found here.


In Practice

To understand how this works in practice please follow the tutorial below.

.feature file

We are given the following Gherkin which tests a remote microservice which checks the amount of candy we have in different situations, according to the scenario in the .feature file.

As you can see there are 2 scenarios:

  • One where we get more candy
  • One where we eat candy
Feature: My candy 
        
  Scenario: I get more candy 
  
    Given I had 3 bags of candy 
    When I am given 1 bags of candy from Jack 
    Then I have 4 bags of candy 

  Scenario: I eat candy 

    Given I had 6 bags of candy 
    When I eat 5 bags of candy  
    Then I have 1 bags of candy 
    And my situation is: 
    | stomach.status | hurts |

.feature file


StepDefs.java File

To run the above Gherkin, we need to create the following methods and annotations. This is done in the StepDefs.java file.

If we do not put any code into the methods, then test will always pass.

public class StepDefs{ 
  private int candy; ​ 

  @Given("I had {int} bags of candy") 
  public void iHadCandy(int number) { 

  } 

  @When("I am given {int} bags of candy from {word}") 
  public void iAmGivenCandy(int number, String person) { ​ 

  } ​ 

  @When("I eat {int} bags of candy")  
  public void iEatCandy(int number) { ​ 

  } ​

  @Then("I have {int} bags of candy") 
  public void iHaveCandy(int number) { ​ 

  } ​ 

  @And("my situation is") 
  public void mySituationIs(Map<String, String> data) { 
 
  } 
}

stub of StepDefs.java file


Annotations

To create a step definition we need to tell Cucumber which method should be triggered for that specific step. This is done using the @ symbol followed by (Given, When, And, Then), which in turn is followed by a round bracket with an expression inside i.e. @Given("I had {int} bags of candy").

As we can see in our examples above the curly brackets ({}) surround the words int and word, which correspond to the Java data-types of integer and String. It is important that our method has those same data-types in the same order as in the annotation as presented in the below example.1 2

@When("I am given {int} bags of candy from {word}")  
public void iAmGivenCandy(int number, String person){}

Note: If we made our method iAmGivenCandy(String person, int number) then it WILL NOT WORK! The name of the method is not important as long as it is unique.


Next Steps

We take a look into the channels of communication and realise that, in most cases we need to just use a web API, but in one case we get data from a queue.

We realise that, since we will need to use JSON and XML formats and talk to an API, we will need a MessageHandler instance which lets us get data from inside a JSON or XML file and helps us work with those formats. We also need a payload which is structured in a format that our web API will accept.

public class StepDefs{ 
  private int candy; 
  private static MessageHandler handler = new MessageHandler();  
  private String payload;

The first method we write, in this case it is mostly a setup step with an assert to make sure that our starting conditions are correct: the API agrees that we have the same amount of candy.

@Given("I had {int} bags of candy") 
public void iHadCandy(int number) { 
  String apicandy = given() 
  .get("http://my.remote.domain/api/candy/check") 
  .asString(); 
  String numberOfCandies = handler.getJsonElement(
                           apicandy, "amountOfCandy"); 
  this.candy = number; 
  assertEquals(Integer.toString(this.candy), numberOfCandies); 
}

The second method will be explained in few steps below. First you can check the whole code.

@When("I am given {int} bags of candy from {word}") 
public void iAmGivenCandy(int number, String person) { 
  String queueName = resolveQueueName(person); 
  assertNotEquals(queueName,"Error"); //it should not be error! 
  QueueConnector queue = new QueueConnector(queueName); 
  String message = queue.getFirstMessage(); 
  String numBags = handler.getXmlElement(message, "numBags"); 
  candy += Integer.parseInt(numBags); 

  handler.setJsonElement(payload, null, "amountOfCandy", 
                         Integer.toString(candy)); 
  given() 
  .when() 
  .contentType(ContentType.JSON) 
  .body(payload) 
  .post("http://my.remote.domain/api/candy/got"); 
}

We will need to create a so-called QueueConnector, connecting to a Queue to get candy from people, maybe it’s our birthday, however, we do not know the queue name, we only know the person giving us the candy, so we use the resolveQueueName method, giving the person’s name, and have it hopefully give us the queue we want to listen to.

@When("I am given {int} bags of candy from {word}") 
public void iAmGivenCandy(int number, String person) { 
  String queueName = resolveQueueName(person);
}

If the resolution fails, the method will return an error, which we need to check against. Thus the assertNotEquals method. There are several other types of asserts found here.

assertNotEquals(queueName,"Error"); //it should not be error!

Once we are sure we actually have our queue, we can create the QueueConnector, giving it our queueName as parameter in its creation (to its constructor).

QueueConnector queue = new QueueConnector(queueName);

We now have a queue we can listen to. In this case we know that there is only one message in that queue, or that we are the only ones checking about it, so we use the getFirstMessage() method. In other cases there may be other methods requiring parameters to only grab messages meant for us. A currently-existing method like that is getNotificationMessage(), which requires a parameter for the VA ID to check against.

String message = queue.getFirstMessage();

We save the message we get into a String, then use our MessageHandler to grab the XML element’s value from the message.

String numBags = handler.getXmlElement(message,"numBags");

Next, we use the value we obtained from the message, to update our local candy count.

candy += Integer.parseInt(numBags);

We put the number into a JSON payload which we then use to tell our API how many new bags of candy we have.

handler.setJsonElement(payload, null, "amountOfCandy",
                       Integer.toString(candy)); 
  given() 
  .when() 
  .contentType(ContentType.JSON) 
  .body(payload) 
  .post("http://my.remote.domain/api/candy/got"); 

Next, we are eating candy, so we need to tell our API how many candy bags we have eaten. So we will write the amount of candy we have eaten into a payload using the MessageHandler, and use restassured to POST it to the remote API.

@When("I eat {int} bags of candy")  
public void iEatCandy(int number) { 
  handler.setJsonElement(payload, null, "amountOfCandy", 
                         Integer.toString(number)); 
  given() 
  .when() 
  .contentType(ContentType.JSON) 
  .body(payload) 
  .post("http://my.remote.domain/api/candy/eaten"); 
}

Next, we need to confirm that the API’s amount of candy bags is identical to ours. To do so we will ask it about how much candy it thinks we should have. Then we will use our MessageHandler to fetch the amount of candy from the response before comparing it with the amount we have in our local variable. If they do not match, we call the fail() method and explain the result.

@Then("I have {int} bags of candy") 
public void iHaveCandy(int number) { 
  Response response = given() 
          .when() 
          .get("http://my.remote.domain/api/candy/check"); 
  if(!handler
         .getJsonElement(response.asString(), "amountOfCandy") 
         .equals(Integer.toString(candy)) 
    ) { 
        fail("the web API messed up the amount of candy bags"); 
      }
}

We know that, if we eat more than 2 bags of candy in one sitting, that our stomach will hurt. Our microservice has been made to also be aware of that, so we will check the status on it, see if it knows our stomach should be hurting now. To do that, we first get the data from it, using restassured. Then we will use a for-each loop to go through the little table we made in the .feature file and search for a key-value pair.

We expect the API response to look something like:

{ 
  "head": {   
    "status":"something" 
  }, 
  ... 
  "stomach": {   
    "status":"something" 
  }, 
  ... 
}

So our .feature file is already made using the stomach.status notation. We now only check that the JSON response has a field stomach.status, and that status is what we expect it to be.

@And("my situation is") 
public void mySituationIs(Map<String, String> data) { 
  ValidatableResponse response = given() 
                     .when() 
                     .get("http://my.remote.domain/api/status") 
                     .then(); 
  for (Map.Entry<String, String> field : data.entrySet()) {
    response.body(field.getKey(), equalTo(field.getValue())); 
  } 
}

In practice the following method could be named differently, have different parameters, and would look into a JSON or YAML structure to find the relevant queue name. It would likely not look like this.

private String resolveQueueName(String name) { 
  if (name.contentEquals("Jack")) { 
    return "MY.QUEUE.NAME"; 
  } 
  else return "Error"; 
}

Please find the example files here.


Arrow-up