Cucumber and Spring Boot Integration: Passing Arguments To Step Definitions Explained
Cucumber is a tool that supports Behavior-Driven Development (BDD). In this blog, you will learn how to pass arguments to step definitions when using Cucumber and Spring Boot. Enjoy!
Introduction
In a previous post, Cucumber was introduced as a tool that supports Behavior-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 on GitHub.
Do check out the following references for extra information:
- Cucumber Expressions
- Cucumber Configuration: Type Registry
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 the previous blog for an introduction)
Application Under Test
The application under test is a basic Spring Boot application. It consists of a Controller and a Service. The Controller serves a customer endpoint that 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
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
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;
...
}
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
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));
}
}
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());
}
}
Conclusion
In this blog, you learned how to integrate Cucumber with a Spring Boot application and several ways to pass arguments to your step definitions - a powerful feature of Cucumber!