Cucumber Tests in Spring Boot with Dependency Injection
This guide shows you how to configure Dependency Injection for your Cucumber tests in Spring Boot.
Cucumber is a great framework to create tests using a BDD approach. Unfortunately, the official website doesn’t provide too much documentation, so it might be tricky to set it up. In this post, I’ll show you how to combine Cucumber with Spring Boot using the cucumber-spring library to leverage Dependency Injection.
Updated (Jan 2025): This guide uses Cucumber 7 and Spring Boot 3.2. If you want to check the changes needed to upgrade from previous versions, have a look at the Pull Request with the changes.
The Spring Boot sample project
I’ll use a practical example to show you how to use Cucumber with Spring. If you want to check out the complete project’s source code, you can clone it from GitHub.
All the code in this post is available on GitHub: Spring Boot Cucumber. If you find it useful, please give it a star!
Let’s first define some minimum functionality: our web application will expose a REST API modeling a bag in which you can put different things via a REST API. Since Cucumber features are the best way to explain the functionality, let me just introduce the main feature we want to implement and test:
Feature: Bag functionalities
Scenario: Putting one thing in the bag
Given the bag is empty
When I put 1 potato in the bag
Then the bag should contain only 1 potato
Scenario: Putting few things in the bag
Given the bag is empty
When I put 1 potato in the bag
And I put 2 cucumber in the bag
Then the bag should contain 1 potato
And the bag should contain 2 cucumber
Important Note: This post does not cover Cucumber and Gherkin concepts. The good news is that there is another complete post on this website covering that, so if you want to learn first how Cucumber and Gherkin work, just check it out.
You can use Cucumber to implement different types of tests. Normally, you use it to cover the ones on top of the test pyramid: integration or end-to-end tests. It’s the place where better fits the needs of mapping business language into feature testing, which usually crosses multiple modules (or components, or microservices…). While the other blog post in this site focuses on microservice end-to-end tests, this one draws the attention on intra-application Integration Tests, covering from the REST API all the way down (without mocks).
If you want to know more about the different ways of testing REST APIs (and Controllers) in Spring, you can also read the Guide to Testing Controllers with Spring Boot.
Required Libraries
We want to use Dependency Injection in our tests with Spring, so we’ll add the cucumber-spring dependency on top of the common cucumber-java and cucumber-junit-platform-engine, needed when you want to write Cucumber tests with Java and JUnit 5.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>io.tpd</groupId>
<artifactId>spring-boot-cucumber</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>spring-boot-cucumber</name>
<description>Demo project using Cucumber DI and Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>17</java.version>
<cucumber.version>7.15.0</cucumber.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-java</artifactId>
<version>${cucumber.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-junit-platform-engine</artifactId>
<version>${cucumber.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-spring</artifactId>
<version>${cucumber.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-suite</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
As you see in the pom.xml file, this post uses Cucumber 7.15 and JUnit 5 (via cucumber-junit-platform-engine).
How Cucumber-Spring works
Overview
The key part of this post: how to make DI work with Spring Boot? It’s simple but not really intuitive. Let’s see the steps.
Even though the sample application has only one controller, BagController, I split the tests into two different Cucumber Features in a way they also make use of two separate StepDefinition classes. This is to show you how to extract common Cucumber steps to a different class that may be shared by your other step-definition classes.
Entry Point: @Suite annotation
First, let’s have a simple class to configure each Cucumber test. This is just a shell that serves as an entry point for the test, together with its configuration. We’ll annotate it with @Suite and configure Cucumber options.
package io.tpd.springbootcucumber.bagbasics;
import org.junit.platform.suite.api.ConfigurationParameter;
import org.junit.platform.suite.api.IncludeEngines;
import org.junit.platform.suite.api.SelectClasspathResource;
import org.junit.platform.suite.api.Suite;
import static io.cucumber.junit.platform.engine.Constants.GLUE_PROPERTY_NAME;
import static io.cucumber.junit.platform.engine.Constants.PLUGIN_PROPERTY_NAME;
@Suite
@IncludeEngines("cucumber")
@SelectClasspathResource("features/bagbasics")
@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "io.tpd.springbootcucumber.bagbasics,io.tpd.springbootcucumber.bagcommons")
@ConfigurationParameter(key = PLUGIN_PROPERTY_NAME, value = "pretty, html:target/cucumber/bagbasics.html")
public class BagCucumberIntegrationTest {
}
The configuration parameters point to the right feature package, configure the plugin for better reporting of tests both in the console output and as HTML, and specify the packages where glue classes may be found. We use it to load configuration and classes that are shared between tests.
You might be wondering at this point how the Spring’s test context is loaded. That is indeed the trickiest part because that’s not configured in the integration test class. We’ll see that when we get there.
Adding dependency injection to Cucumber tests
The piece of code that brings dependency injection to our Cucumber tests is the CucumberSpringConfiguration class. The @CucumberContextConfiguration annotation tells Cucumber to use this class as the test context configuration for Spring. In our case, we don’t need to change the configuration, nor we add extra beans here, but all that is possible within this class. Since we’re using Spring Boot, we can annotate this configuration class with @SpringBootTest. Note that this is confusing, since this class isn’t a test, but it’s just the convention that the Cucumber team decided to use. Check the Cucumber Spring README file on GitHub for more information and extra options.
package io.tpd.springbootcucumber.bagcommons;
import io.cucumber.spring.CucumberContextConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
@CucumberContextConfiguration
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class CucumberSpringConfiguration {
}
A Test Component
Since we want to use the Spring context and dependency injection, let’s build a component to put that into practice. This class will abstract all the client’s API connections, so we don’t have to duplicate it within our Cucumber tests.
package io.tpd.springbootcucumber.bagcommons;
import io.tpd.springbootcucumber.Bag;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import static io.cucumber.spring.CucumberTestContext.SCOPE_CUCUMBER_GLUE;
@Component
@Scope(SCOPE_CUCUMBER_GLUE)
public class BagHttpClient {
private final String SERVER_URL = "http://localhost";
private final String THINGS_ENDPOINT = "/things";
@LocalServerPort
private int port;
private final RestTemplate restTemplate = new RestTemplate();
private String thingsEndpoint() {
return SERVER_URL + ":" + port + THINGS_ENDPOINT;
}
public int put(final String something) {
return restTemplate.postForEntity(thingsEndpoint(), something, Void.class).getStatusCode().value();
}
public Bag getContents() {
return restTemplate.getForEntity(thingsEndpoint(), Bag.class).getBody();
}
public void clean() {
restTemplate.delete(thingsEndpoint());
}
}
As you see, this class is just providing common functionalities to our test. In this case, I created a RestTemplate and generic methods to use the application functionalities. The random port used by the test gets injected into the class as configured with the @LocalServerPort annotation.
The “cucumber-glue” scope tells Cucumber to remove this bean and recreate a new one if needed after each scenario. Here it’s just a way to keep everything clean but in some other cases might be very useful, e.g. if you alter context beans during a test.
The Step Definition classes
Injecting a component into a Cucumber step definition class
First, let’s have a look at the BagCucumberStepDefinitions class. It defines most of the steps needed in one of our Features, defined in the bag.feature file.
package io.tpd.springbootcucumber.bagbasics;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;
import io.tpd.springbootcucumber.bagcommons.BagHttpClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import java.util.Collections;
import java.util.List;
import java.util.stream.IntStream;
import static org.assertj.core.api.Assertions.assertThat;
public class BagCucumberStepDefinitions {
private final Logger log = LoggerFactory.getLogger(BagCucumberStepDefinitions.class);
@Autowired
private BagHttpClient bagHttpClient;
@When("^I put (\\d+) (\\w+) in the bag$")
public void i_put_something_in_the_bag(final int quantity, final String something) {
IntStream.range(0, quantity)
.peek(n -> log.info("Putting a {} in the bag, number {}", something, quantity))
.map(ignore -> bagHttpClient.put(something))
.forEach(statusCode -> assertThat(statusCode).isEqualTo(HttpStatus.CREATED.value()));
}
@Then("^the bag should contain only (\\d+) (\\w+)$")
public void the_bag_should_contain_only_something(final int quantity, final String something) {
assertNumberOfTimes(quantity, something, true);
}
@Then("^the bag should contain (\\d+) (\\w+)$")
public void the_bag_should_contain_something(final int quantity, final String something) {
assertNumberOfTimes(quantity, something, false);
}
private void assertNumberOfTimes(final int quantity, final String something, final boolean onlyThat) {
final List<String> things = bagHttpClient.getContents().getThings();
log.info("Expecting {} times {}. The bag contains {}", quantity, something, things);
final int timesInList = Collections.frequency(things, something);
assertThat(timesInList).isEqualTo(quantity);
if (onlyThat) {
assertThat(timesInList).isEqualTo(things.size());
}
}
}
Thanks to the dependency cucumber-spring and the class annotated with @CucumberContextConfiguration, Cucumber will load the Spring Boot context and will add the BagHttpClient component to it. In the step definitions classes, we can inject the beans using Spring’s @Autowired annotation.
Keep in mind that your IDE might complain about this since this approach is a bit odd: @Autowired doesn’t normally work in classes that are not Spring beans. Again, this is just a convention that the Cucumber team decided to implement.
Adding a second feature and shared steps
To make this case more representative, I created a second feature that makes use of some common steps. Both feature tests are configured to get all glue classes from within the specified packages via the GLUE_PROPERTY_NAME configuration parameter.
@Suite
@IncludeEngines("cucumber")
@SelectClasspathResource("features/bagextra")
@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "io.tpd.springbootcucumber.bagextra,io.tpd.springbootcucumber.bagcommons")
@ConfigurationParameter(key = PLUGIN_PROPERTY_NAME, value = "pretty, html:target/cucumber/bagextra.html")
public class BagExtraCucumberIntegrationTest {
}
This is the class where we load the common step definition that we use in both features:
package io.tpd.springbootcucumber.bagcommons;
import io.cucumber.java.en.Given;
import org.springframework.beans.factory.annotation.Autowired;
import static org.assertj.core.api.Assertions.assertThat;
public class BagCommonCucumberStepDefinitions {
@Autowired
private BagHttpClient bagHttpClient;
@Given("^the bag is empty$")
public void the_bag_is_empty() {
bagHttpClient.clean();
assertThat(bagHttpClient.getContents().isEmpty()).isTrue();
}
}
The HTTP client is injected via the @Autowired annotation, same as before.
Running the Application
You can now run the tests from your favorite IDE or using Maven:
mvn clean test
You’ll see how Cucumber prints the steps and the logs in between them:
Scenario: Putting one thing in the bag
Given the bag is empty
When I put 1 potato in the bag
Then the bag should contain only 1 potato
Scenario: Putting few things in the bag
Given the bag is empty
When I put 1 potato in the bag
And I put 2 cucumber in the bag
Then the bag should contain 1 potato
And the bag should contain 2 cucumber
That will only happen if you set the plugin “pretty” in the configuration parameters (like we did in this example). Since we also set the option to generate HTML reports, we’ll have them available in the target/cucumber folder.
Related Content
- I wrote another post about End-to-End Microservice tests with Cucumber, which gives you more details about this framework. Check it out here.
- This is an integration test but there are many ways to test just the Controller layer in Spring, or include some other parts in them. Learn about the different ways of testing Controllers in Spring Boot.
- If you want to know much more about Cucumber and how to build a Microservices Architecture from scratch, have a look at my book, you will like it.
Get More Architecture Insights
Join hundreds of software architects and engineers getting practical insights delivered to their inbox.