Cucumber Tests in Spring Boot with Dependency Injection

Cucumber Tests in Spring Boot with Dependency Injection

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.

Table of Contents

The Spring Boot sample project

Following this blog’s style, 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 same website doing 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

Given that we want to use Cucumber within the Spring Boot application, we would like it to load all the Web Application Context including our configured Beans. Therefore, we need to add the dependency cucumber-spring to our project, apart from Cucumber’s basic ones. We’ll cover later how to make it work.

<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</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>

If you prefer to build your own project from scratch following this post, just use the Spring Initializer and select Spring Boot 2, Maven, and the Web dependency. As ${cucumber.version} I used 4.7.0 (configured above in the same pom.xml file).

How Cucumber-Spring works

Get the book Practical Software Architecture

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.

UPDATED [August 2019]: The first version of this post used Cucumber 2 and a single feature. Now it is updated to cover Cucumber 4 and demonstrate how multiple Cucumber Step Definitions can be loaded within the same Test’s Application Context. The reason is that the Cucumber Java team has introduced some restrictions which made the previous code not work with multiple features.

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. The idea is showing you how to extract common Cucumber steps to a different class that may be shared by your other step-definition classes.

Spring Boot Cucumber Setup
Spring Boot Cucumber Setup

Entry Point: @RunWith Cucumber

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 @RunWith(Cucumber.class) to instruct JUnit to use this runner so we have all the Cucumber functionality.

All the code in this post is available on GitHub: Spring Boot Cucumber. If you find it useful, please give it a star!
package io.tpd.springbootcucumber.bagbasics;

import io.cucumber.junit.Cucumber;
import io.cucumber.junit.CucumberOptions;
import org.junit.runner.RunWith;

@RunWith(Cucumber.class)
@CucumberOptions(features = "src/test/resources/features/bagbasics",
        plugin = {"pretty", "html:target/cucumber/bagbasics"},
        extraGlue = "io.tpd.springbootcucumber.bagcommons")
public class BagCucumberIntegrationTest {
}

The @CucumberOptions annotation is responsible for pointing to the right feature package, configuring the plugin for a better reporting of tests both in the console output and as HTML, and specifying the package where extraGlue 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: it’s not here, we need to do that via our StepDefinitions class. We’ll see that when we get there.

Note that, in this case, we have only one feature per Integration Test (that resource package only contains the bag.feature file). We could add more features within the same package and this example would still work, as long as we make sure that we group all steps together or only one of our StepDefinitions classes loads the Spring Context. See the dedicated section below for more details.

Adding DI to Cucumber Steps’ definition class

The part that connects our Cucumber test so it runs with a Spring Boot context is the steps definition class. It’s there where we define the steps required by our .feature file, and we’ll annotate it in this case with the @SpringBootTest annotation.

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.boot.test.context.SpringBootTest;
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;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
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());
        }
    }

}

Now, thanks to the added dependency cucumber-spring, Cucumber will load the context when pulling the steps from that class. Note that we use here a random port for our test and also a test-specific autowired component.

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 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.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).getStatusCodeValue();
    }

    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.

Adding a second feature and shared steps

In a previous version of this post, some of you pointed out via comments that the proposed solution at that time wouldn’t work with more than one feature after upgrading Cucumber. The error was something like:

cucumber.runtime.CucumberException: Glue class XSteps and class YSteps both 
    attempt to configure the spring context.
Please ensure only one glue class configures the spring context.

The reason is that now it is not allowed anymore to have two classes annotated with @SpringBootTest or extending a class with that annotation if they’re part of the same test. The solution is to structure the test code a bit and use the extraGlue parameter whenever you want to use some extra classes.

Get the book Practical Software Architecture

In the example, I created a second feature that make use of some common steps. Both feature tests are configured by default to get all glue classes from within the same package so that parameter is omitted in my @CucumberOptions annotation. However, they both want to use the class BagCommonCucumberStepDefinitions located in a different package so that’s the reason for the extraGlue parameter.

@RunWith(Cucumber.class)
@CucumberOptions(features = "src/test/resources/features/bagextra",
        plugin = {"pretty", "html:target/cucumber/bagextra"},
        extraGlue = "io.tpd.springbootcucumber.bagcommons")
public class BagExtraCucumberIntegrationTest {
}

You have to make sure that none of the classes in that package tries to load another Spring context, that’s why it’s very important that your shared steps don’t use the @SpringBootTest annotation. It’s possible to use dependency injection anyway since the context is loaded by your “main” step definitions class for that test.

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();
    }

}

Running the Application

You can now run the tests from your favorite IDE or using Maven:

$ mvn clean test

And you’ll see Cucumber printing the steps and the logs in between them:

Feature: Bag functionalities

  Scenario: Putting one thing in the bag # src/test/resources/features/bagextra/bag-more.feature:3
2019-08-19 18:44:43.730  INFO 87862 --- [o-auto-1-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring FrameworkServlet 'dispatcherServlet'
2019-08-19 18:44:43.730  INFO 87862 --- [o-auto-1-exec-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization started
2019-08-19 18:44:43.746  INFO 87862 --- [o-auto-1-exec-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization completed in 16 ms
    Given the bag is not empty           # BagExtraCucumberStepDefinitions.the_bag_is_not_empty()
    When I empty the bag                 # BagExtraCucumberStepDefinitions.empty_the_bag()
    Then the bag is empty                # BagCommonCucumberStepDefinitions.the_bag_is_empty()
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.39 s - in io.tpd.springbootcucumber.bagextra.BagExtraCucumberIntegrationTest
...
Feature: Bag functionalities

  Scenario: Putting one thing in the bag      # src/test/resources/features/bagbasics/bag.feature:3
    Given the bag is empty                    # BagCommonCucumberStepDefinitions.the_bag_is_empty()
2019-08-19 18:44:43.979  INFO 87862 --- [           main] i.t.s.b.BagCucumberStepDefinitions       : Putting a potato in the bag, number 1
    When I put 1 potato in the bag            # BagCucumberStepDefinitions.i_put_something_in_the_bag(int,String)
2019-08-19 18:44:44.010  INFO 87862 --- [           main] i.t.s.b.BagCucumberStepDefinitions       : Expecting 1 times potato. The bag contains [potato]
    Then the bag should contain only 1 potato # BagCucumberStepDefinitions.the_bag_should_contain_only_something(int,String)
...
Scenario: Putting few things in the bag # src/test/resources/features/bagbasics/bag.feature:8
    Given the bag is empty                # BagCommonCucumberStepDefinitions.the_bag_is_empty()
2019-08-19 18:44:44.036  INFO 87862 --- [           main] i.t.s.b.BagCucumberStepDefinitions       : Putting a potato in the bag, number 1
    When I put 1 potato in the bag        # BagCucumberStepDefinitions.i_put_something_in_the_bag(int,String)
2019-08-19 18:44:44.043  INFO 87862 --- [           main] i.t.s.b.BagCucumberStepDefinitions       : Putting a cucumber in the bag, number 2
2019-08-19 18:44:44.049  INFO 87862 --- [           main] i.t.s.b.BagCucumberStepDefinitions       : Putting a cucumber in the bag, number 2
    And I put 2 cucumber in the bag       # BagCucumberStepDefinitions.i_put_something_in_the_bag(int,String)
2019-08-19 18:44:44.066  INFO 87862 --- [           main] i.t.s.b.BagCucumberStepDefinitions       : Expecting 1 times potato. The bag contains [potato, cucumber, cucumber]
    Then the bag should contain 1 potato  # BagCucumberStepDefinitions.the_bag_should_contain_something(int,String)
2019-08-19 18:44:44.073  INFO 87862 --- [           main] i.t.s.b.BagCucumberStepDefinitions       : Expecting 2 times cucumber. The bag contains [potato, cucumber, cucumber]
    And the bag should contain 2 cucumber # BagCucumberStepDefinitions.the_bag_should_contain_something(int,String)
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.152 s - in io.tpd.springbootcucumber.bagbasics.BagCucumberIntegrationTest

That will only happen if you set the plugin “pretty” in @CucumberOptions (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. Don’t expect fancy reports, it’s very simple - yet functional.

Moisés Macero's Picture

About Moisés Macero

Software Developer, Architect, and Author.
Check my workshops

Amsterdam, The Netherlands https://thepracticaldeveloper.com

Comments