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

Updated (Feb 17th, 2021): This guide uses now Cucumber 6. If you want to check the changes needed to upgrade from Cucumber Spring version 4 to 6, have a look at the Pull Request

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

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, needed when you want to write Cucumber tests with Java and JUnit.

<?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>2.0.0.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <cucumber.version>6.8.1</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</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.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 has been updated to use Cucumber 6.8.

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.

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.

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 because that’s not configured in the integration test class. We’ll see that when we get there.

Note that, in this case, we have only one feature per Integration Test (the resource package referenced in the annotation only contains the bag.feature file). We could add more features within the same package and this example would still work.

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

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 the 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, which is a great improvement in comparison to how it used to work in previous Cucumber versions (if you’re curious, have a look at this post’s code for Cucumber 4 on GitHub).

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 Cucumber 4 didn’t allow 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 at that time was to structure the test code a bit and use the extraGlue parameter whenever you want to use some extra classes.

The good news is that new versions of Cucumber have simplified how to work with Spring Boot and dependency injection in Cucumber tests. Now, we don’t need to do advanced tricks or annotate only one of the classes to make our test suite work. The Context Configuration has been extracted to a separate class, as we already covered.

Get the book Practical Software Architecture

To make this case more representative, I created a second feature that makes 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 {
}

In Cucumber 4, you had to make sure that none of the classes in that package tried to load another Spring context via @SpringBootTest annotation. Now, this annotation is located at the context configuration class, making our life easier.

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      # src/test/resources/features/bagbasics/bag.feature:3
  Given the bag is empty                    # io.tpd.springbootcucumber.bagcommons.BagCommonCucumberStepDefinitions.the_bag_is_empty()
2021-02-17 07:42:27.152  INFO 25870 --- [           main] i.t.s.b.BagCucumberStepDefinitions       : Putting a potato in the bag, number 1
  When I put 1 potato in the bag            # io.tpd.springbootcucumber.bagbasics.BagCucumberStepDefinitions.i_put_something_in_the_bag(int,java.lang.String)
2021-02-17 07:42:27.167  INFO 25870 --- [           main] i.t.s.b.BagCucumberStepDefinitions       : Expecting 1 times potato. The bag contains [potato]
  Then the bag should contain only 1 potato # io.tpd.springbootcucumber.bagbasics.BagCucumberStepDefinitions.the_bag_should_contain_only_something(int,java.lang.String)
...
Scenario: Putting few things in the bag # src/test/resources/features/bagbasics/bag.feature:8
  Given the bag is empty                # io.tpd.springbootcucumber.bagcommons.BagCommonCucumberStepDefinitions.the_bag_is_empty()
2021-02-17 07:42:27.194  INFO 25870 --- [           main] i.t.s.b.BagCucumberStepDefinitions       : Putting a potato in the bag, number 1
  When I put 1 potato in the bag        # io.tpd.springbootcucumber.bagbasics.BagCucumberStepDefinitions.i_put_something_in_the_bag(int,java.lang.String)
2021-02-17 07:42:27.201  INFO 25870 --- [           main] i.t.s.b.BagCucumberStepDefinitions       : Putting a cucumber in the bag, number 2
2021-02-17 07:42:27.205  INFO 25870 --- [           main] i.t.s.b.BagCucumberStepDefinitions       : Putting a cucumber in the bag, number 2
  And I put 2 cucumber in the bag       # io.tpd.springbootcucumber.bagbasics.BagCucumberStepDefinitions.i_put_something_in_the_bag(int,java.lang.String)
2021-02-17 07:42:27.213  INFO 25870 --- [           main] i.t.s.b.BagCucumberStepDefinitions       : Expecting 1 times potato. The bag contains [potato, cucumber, cucumber]
  Then the bag should contain 1 potato  # io.tpd.springbootcucumber.bagbasics.BagCucumberStepDefinitions.the_bag_should_contain_something(int,java.lang.String)
2021-02-17 07:42:27.216  INFO 25870 --- [           main] i.t.s.b.BagCucumberStepDefinitions       : Expecting 2 times cucumber. The bag contains [potato, cucumber, cucumber]
  And the bag should contain 2 cucumber # io.tpd.springbootcucumber.bagbasics.BagCucumberStepDefinitions.the_bag_should_contain_something(int,java.lang.String)

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.

  • 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.
Moisés Macero's Picture

About Moisés Macero

Software Developer, Architect, and Author.
Are you interested in my workshops?

Málaga, Spain https://thepracticaldeveloper.com

Comments

About Moisés Macero

Software Developer, Architect, and Author.
Are you interested in my workshops?

Learn microservices architecture with my book

Latest Posts

  • Book's Upgrade: Migrating from Spring Boot 2.6 to 2.7 August 5, 2022
  • Book's Upgrade: Migrating from Spring Boot 2.5 to 2.6 January 21, 2022
  • Book's Upgrade: Migrating from Spring Boot 2.4 to 2.5 October 15, 2021
  • How to test a controller in Spring Boot - a practical guide October 9, 2021
  • Spring Boot and Kafka – Practical Example October 8, 2021

Tags

JSON REST agile angular architecture async bdd book book-2nd conferences cucumber docker eureka feature-toggle gamification hystrix java jboss junit kafka microservices mongodb rabbitmq reactive ribbon spring-boot swagger test webflux wildfly zuul

Sign up to get a discount

Buy on Amazon

4617作文网姓谢的男孩起什么名字好喜马拉雅APP西餐厅十大品牌加盟游戏怎么起名起名属鼠女孩起名的笔画数多少好呢写小说起名字新版包青天全集阴户孩子起名木字旁的崔姓起名称大全男孩几内亚经商处草根石布衣唐诗宋词里面起名字舞团的名字用诗起姓名大全李安安褚逸辰免费无弹窗阅读小说济南个人房屋出租五行属火的字女孩起名免费宝宝起名测分数梦见牙齿脱落猪年的宝宝起名用字日月起名网起名字大全女孩免费打分剧透诸天万界起酒名大全列表注册公司起名字去哪里查重7电影网德州区号聂氏起名淀粉肠小王子日销售额涨超10倍罗斯否认插足凯特王妃婚姻让美丽中国“从细节出发”清明节放假3天调休1天男子给前妻转账 现任妻子起诉要回网友建议重庆地铁不准乘客携带菜筐月嫂回应掌掴婴儿是在赶虫子重庆警方辟谣“男子杀人焚尸”国产伟哥去年销售近13亿新的一天从800个哈欠开始男孩疑遭霸凌 家长讨说法被踢出群高中生被打伤下体休学 邯郸通报男子持台球杆殴打2名女店员被抓19岁小伙救下5人后溺亡 多方发声单亲妈妈陷入热恋 14岁儿子报警两大学生合买彩票中奖一人不认账德国打算提及普京时仅用姓名山西省委原副书记商黎光被逮捕武汉大学樱花即将进入盛花期今日春分张家界的山上“长”满了韩国人?特朗普谈“凯特王妃P图照”王树国3次鞠躬告别西交大师生白宫:哈马斯三号人物被杀代拍被何赛飞拿着魔杖追着打315晚会后胖东来又人满为患了房客欠租失踪 房东直发愁倪萍分享减重40斤方法“重生之我在北大当嫡校长”槽头肉企业被曝光前生意红火手机成瘾是影响睡眠质量重要因素考生莫言也上北大硕士复试名单了妈妈回应孩子在校撞护栏坠楼网友洛杉矶偶遇贾玲呼北高速交通事故已致14人死亡西双版纳热带植物园回应蜉蝣大爆发男孩8年未见母亲被告知被遗忘张立群任西安交通大学校长恒大被罚41.75亿到底怎么缴沈阳一轿车冲入人行道致3死2伤奥运男篮美国塞尔维亚同组周杰伦一审败诉网易国标起草人:淀粉肠是低配版火腿肠外国人感慨凌晨的中国很安全男子被流浪猫绊倒 投喂者赔24万杨倩无缘巴黎奥运男子被猫抓伤后确诊“猫抓病”春分“立蛋”成功率更高?记者:伊万改变了国足氛围奥巴马现身唐宁街 黑色着装引猜测

4617作文网 XML地图 TXT地图 虚拟主机 SEO 网站制作 网站优化