Link Search Menu Expand Document

Configuring a Java Test Framework Using Cucumber 7, Junit 5, and Spring Boot

Table of contents
  1. Dependencies and POM Configuration
  2. Test Runner Configuration
  3. Spring Integration Configuration
    1. Spring Application Context Configuration
  4. Step Definitions
  5. References

First of all, I would like to thank Palash that inspired me for this post and provided an MVP of Spring + Cucumber + Junit5 which works (see here the original post).

Dependencies and POM Configuration

Starting from Cucumber 7, the development team introduced a BOM which makes the dependencies management easier. So did the Junit development team. Besides Cucumber and Junit dependencies, let’s add the Spring Boot parent POM dependency along with some utilities like Lombok, some logging (e.g., Log4J) and AssertJ.

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.5.5</version>
    <relativePath/>
</parent>

<properties>
    <org.projectlombok.version>1.18.22</org.projectlombok.version>
    <org.apache.logging.log4j.version>2.17.1</org.apache.logging.log4j.version>

    <org.junit.jupiter.version>5.8.2</org.junit.jupiter.version>
    <io.cucumber.version>7.2.2</io.cucumber.version>

    <assertj-core.version>3.22.0</assertj-core.version>
</properties>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>io.cucumber</groupId>
            <artifactId>cucumber-bom</artifactId>
            <version>${io.cucumber.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <dependency>
            <groupId>org.junit</groupId>
            <artifactId>junit-bom</artifactId>
            <version>${org.junit.jupiter.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <!-- Spring dependencies -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>${org.projectlombok.version}</version>
        <scope>provided</scope>
    </dependency>

    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-core</artifactId>
        <version>${org.apache.logging.log4j.version}</version>
    </dependency>

    <!-- Test dependencies -->
    <dependency>
        <groupId>io.cucumber</groupId>
        <artifactId>cucumber-java</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>io.cucumber</groupId>
        <artifactId>cucumber-spring</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>io.cucumber</groupId>
        <artifactId>cucumber-junit-platform-engine</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.junit.platform</groupId>
        <artifactId>junit-platform-suite</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-api</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.assertj</groupId>
        <artifactId>assertj-core</artifactId>
        <version>${assertj-core.version}</version>
    </dependency>
</dependencies>

There are quite many dependencies for a hello-world project, but what can we do…

The key parts in the given configuration represent the io.cucumber:cucumber-junit-platform-engine and org.junit.platform:junit-platform-suite. In Cucumber versions prior to 7 it was used @Cucumber annotation to mark the test runner. Starting from Cucumber 7, this annotation is deprecated in favour of JUnit Platform Suite.

Test Runner Configuration

The suite configuration consists of two parts: Cucumber Spring context configuration and the Cucumber test runner configuration. The test runner is configured using JUnit Platform Suite

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.core.options.Constants.FILTER_TAGS_PROPERTY_NAME;
import static io.cucumber.core.options.Constants.GLUE_PROPERTY_NAME;
import static io.cucumber.core.options.Constants.PLUGIN_PUBLISH_QUIET_PROPERTY_NAME;

@Suite
@IncludeEngines("cucumber")
@SelectClasspathResource("bdd")
@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "com.example.bdd")
@ConfigurationParameter(key = PLUGIN_PUBLISH_QUIET_PROPERTY_NAME, value = "true")
@ConfigurationParameter(key = FILTER_TAGS_PROPERTY_NAME, value = "@RunMe and not @Skip")
public class RunCucumberTests {}

In case the configuration of your project is split across several packages and you need to add them to GLUE, specifying the packages names as comma separated values. In our case, com.example.bdd is the base package under which our application code is developed. Thus, it is sufficient to add the entire base package to GLUE.

The @SelectClasspathResource("bdd") annotation tells Cucumber to look for the feature files under “classpath:bdd/” folder or “src/test/resources/bdd/”. Additional configuration parameters can be passed to Junit using @ConfigurationParameter.

Spring Integration Configuration

Our next step would be to tie the Spring context to our Cucumber test context. We can achieve this by combining the @CucumberContextConfiguration with our Spring test configuration.

import com.example.config.SpringTestConfig;
import io.cucumber.spring.CucumberContextConfiguration;
import org.springframework.boot.test.context.SpringBootTest;

@CucumberContextConfiguration
@SpringBootTest(
    webEnvironment = SpringBootTest.WebEnvironment.NONE, 
    classes = SpringTestConfig.class
)
public class CucumberSpringContextConfig {}

Spring Application Context Configuration

We could implement our step definitions even here, although the best practices advise us to separate them to another file. The SpringTestConfig.class, as one can deduce by the name, is the place where our Spring test configuration resides.

import com.example.bdd.custom.CustomLogger;
import lombok.extern.log4j.Log4j2;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@Log4j2
@TestConfiguration
public class SpringTestConfig {

    @Bean
    public CustomLogger customLogger() {
        return new CustomLogger();
    }

    @Bean
    public RestTemplate restTemplate() {
        log.info("Creating new RestTemplate bean");
        return new RestTemplate();
    }
}

For example, in the given configuration we configure two beans which we can then inject into our step definition class.

Step Definitions

The step definition classes are seen by Spring as component classes. Thus, we can inject other beans/components and use them.

The best practices advise us to split our step definitions according to their application domain.

import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;

@Log4j2
@RequiredArgsConstructor
public class AccessibilityStepDefinitions {

    @Given("The application is available at {string}")
    public void applicationBaseUrl(final String baseUrl) {
        log.info("Application is available at {}", baseUrl);
    }

    @Given("^Health check is fine at (.*)$")
    public void checkApplicationHealth(final String uri) {
        log.info("Application is healthy! {}", uri);
    }

    @Given("A valid connection to database is prepared")
    public void givenAValidConnectionToDatabaseIsPrepared() {
        log.info("Connection valid");
    }

    @When("The available connection to database is closing")
    public void whenTheAvailableConnectionToDatabaseIsClosing() {
        log.info("Connection closing");
    }

    @Then("The connection to the database is closed")
    public void thenTheConnectionToTheDatabaseIsClosed() {
        log.info("Connection closed");
    }
}

Additionally, we may have, for example, DatabaseStepDefinitions which would be used by Cucumber. Having the above step definition, we may create the src/test/resources/bdd/accessibility.feature file with the following content:

Feature: Test our service's rest endpoints

  Background: The application is reachable and healthy
    Given The application is available at "http://localhost:8080"
    And Health check is fine at "/actuator/health"

  @RunMe
  Scenario: Open and close a connection to database 1-1
    Given A valid connection to database is prepared
    When The available connection to database is closing
    Then The connection to the database is closed

References

  1. Example of creating Cucumber based BDD tests using JUnit5 and Spring Dependency Injection, https://palashray.com/example-of-creating-cucumber-based-bdd-tests-using-junit5-and-spring-dependency-injection/
  2. Cucumber Spring, https://github.com/cucumber/cucumber-jvm/tree/main/spring
  3. Cucumber JUnit Platform Engine, https://github.com/cucumber/cucumber-jvm/tree/main/junit-platform-engine