In this blog, you will learn how to pass arguments to step definitions when using Cucumber and Spring Boot. Cucumber is a tool that supports Behaviour-Driven Development (BDD). Enjoy!

1. Introduction

In a previous post, Cucumber was introduced as a tool that supports Behaviour-Driven Development (BDD). Some of the features were explained, but not how to pass arguments to step definitions. In this blog, you will learn how you can do so. The application under test is a Spring Boot application. You will also learn how you can integrate the Cucumber tests with Spring.

The sources used in this blog are available at GitHub.

Do check out the following references for extra information:

2. Prerequisites

The prerequisites for this blog are:

  • Basis Java knowledge, Java 21 is used;
  • Basic Maven knowledge;
  • Basic Spring Boot knowledge;
  • Basic comprehension of BDD;
  • Basic knowledge of Cucumber, see a previous blog for an introduction.

3. Application Under Test

The application under test is a basic Spring Boot application. It consists out of a Controller and a Service. The Controller serves a customer endpoint which implements an OpenAPI specification. The Service is a basic implementation, storing customers in a HashMap. A customer only has a first name and a last name, just to keep things simple. The API offers the following functionality:

  • creating a customer;
  • retrieving the customer based on the customer ID;
  • retrieving all customers;
  • deleting all customers.

4. Spring Integration

In order to enable the Spring integration, you add the following dependency to the pom:

<dependency>
  <groupId>io.cucumber</groupId>
  <artifactId>cucumber-spring</artifactId>
  <version>7.14.0</version>
  <scope>test</scope>
</dependency>

The Spring Boot application must be in a running state, therefore you need to run the Cucumber tests with the @SpringBootTest annotation. This will start the application and you will be able to run Cucumber tests for it. In order to do so, you create a class CucumberSpringConfiguration. Add the @CucumberContextConfiguration annotation so that the Spring integration is enabled. The Spring Boot application starts on a random port, therefore you store the port in a system property so that you will be able to use it when you need to call the API.

@CucumberContextConfiguration
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class CucumberSpringConfiguration {
    @LocalServerPort
    private int port;

    @PostConstruct
    public void setup() {
        System.setProperty("port", String.valueOf(port));
    }
}

The Cucumber step definitions will extend this class.

Tests can be run via Maven:

$ mvn clean verify

5. Test: Add Customer – Using Arguments

The Add Customer test will add a customer to the customer list and will verify that the customer is added to the list. The feature file is the following. Do note that the first name (John) and last name (Doe) are within quotes. This way, Cucumber is able to recognize a string argument.

Scenario: Add customer
    Given an empty customer list
    When customer 'John' 'Doe' is added
    Then the customer 'John' 'Doe' is added to the customer list

The corresponding step definitions are the following.

  • When: the first name and last name placeholders are represented with {string} and they are mapped as arguments to the method. This way, the arguments are accessible to the step definition.
  • Then: in a similar way, the arguments are passed to the step definition.
public class StepDefinitions extends CucumberSpringConfiguration {

    final int port = Integer.parseInt(System.getProperty("port"));
    final RestClient restClient = RestClient.create();

    @Given("an empty customer list")
    public void an_empty_customer_list() {
        ResponseEntity<Void> response = restClient.delete()
                .uri("http://localhost:"+ port + "/customer")
                .retrieve()
                .toBodilessEntity();
    }

    @When("customer {string} {string} is added")
    public void customer_firstname_lastname_is_added(String firstName, String lastName) {
        Customer customer = new Customer(firstName, lastName);
        ResponseEntity<Void> response = restClient.post()
                .uri("http://localhost:"+ port + "/customer")
                .contentType(APPLICATION_JSON)
                .body(customer)
                .retrieve()
                .toBodilessEntity();
        assertThat(response.getStatusCode().is2xxSuccessful()).isTrue();
    }

    @Then("the customer {string} {string} is added to the customer list")
    public void the_customer_first_name_last_name_is_added_to_the_customer_list(String firstName, String lastName) {
        List<Customer> customers = restClient.get()
                .uri("http://localhost:"+ port + "/customer")
                .retrieve()
                .body(new ParameterizedTypeReference<>() {});
        assertThat(customers).contains(new Customer(firstName, lastName));
    }
    ...
}

Note that the arguments are used to create a Customer object which is defined in the step definitions class. This class contains the fields, getters, setters, equals and hashCode implementations.

public static class Customer {
        private String firstName;
        private String lastName;
        ...
}

6. Test: Add Customers – Using Arguments

When you want to add several customers, you can chain the same step definition by means of an And using different arguments. The feature file is the following, the step definitions remain the same.

Scenario: Add customers
    Given an empty customer list
    When customer 'John' 'Doe' is added
    And customer 'David' 'Beckham' is added
    Then the customer 'John' 'Doe' is added to the customer list
    And the customer 'David' 'Beckham' is added to the customer list

7. Test: Add Customer – Using DataTable

The previous tests all started with an empty customer list. The next test will add some data to the customer list as a starting point. You can of course use the step definition customer firstName lastName is added and invoke it multiple times, but you can also use a DataTable. The DataTable must be the last argument in a step definition. The feature file is the following and the DataTable is used in the Given-clause.

Scenario: Add customer to existing customers
    Given the following customers:
      | John  | Doe     |
      | David | Beckham |
    When customer 'Bruce' 'Springsteen' is added
    Then the customer 'Bruce' 'Springsteen' is added to the customer list

In the implementation of the step definition, you now see that the arguments are passed as a DataTable. It is a table containing strings, so you need to parse the table yourself.

@Given("the following customers:")
public void the_following_customers(io.cucumber.datatable.DataTable dataTable) {

    for (List<String> customer : dataTable.asLists()) {
        customer_firstname_lastname_is_added(customer.get(0), customer.get(1));
    }
}

8. Test: Add Customer – Using Parameter Type

In the previous test, you needed to parse the DataTable yourself. Wouldn’t it be great if the DataTable could be mapped immediately to a Customer object? This is possible if you define a parameter type for it. You create a parameter type customerEntry and annotate it with @DataTableType. You use the string arguments of a DataTable to create a Customer object. You do so in a class ParameterTypes, which is considered as best practice.

public class ParameterTypes {
    @DataTableType
    public StepDefinitions.Customer customerEntry(Map<String, String> entry) {
        return new StepDefinitions.Customer(
                entry.get("firstName"),
                entry.get("lastName"));
    }
}

The feature file is identical to the previous one, only the step definition has changed in order to have a unique step definition.

Scenario: Add customer to existing customers with parameter type
    Given the following customers with parameter type:
      | John  | Doe     |
      | David | Beckham |
    When customer 'Bruce' 'Springsteen' is added
    Then the customer 'Bruce' 'Springsteen' is added to the customer list

In the implementation of the step definition, you notice that the argument is not a DataTable anymore, but a list of Customer.

@Given("the following customers with parameter type:")
public void the_following_customers_with_parameter_type(List<Customer> customers) {

    for (Customer customer : customers) {
        customer_firstname_lastname_is_added(customer.getFirstName(), customer.getLastName());
    }
}

9. Conclusion

In this blog, you learned how to integrate Cucumber with a Spring Boot application and several ways how to pass arguments to your step definitions. A powerful feature of Cucumber!