Link Search Menu Expand Document

Configuring Multiple Java Test Frameworks Using Cucumber 7, Junit 5, and Spring Boot

Table of contents
  1. Spring Configurations Based on Profiles
  2. Cucumber Test Suites Configuration
    1. Cucumber Spring Context Configuration
    2. Cucumber Test Runner Configuration
  3. Step Definitions
  4. Feature File and Scenarios
  5. File Structure
  6. (Bonus) Maven Surefire Plugin Options
  7. Disadvantages of the Proposed Model
  8. References

In my previous post I have described how to set up a Java test framework using Cucumber 7, Junit 5, and Spring Boot. In this post, we will extend the previous configuration in order to set up multiple Cucumber test executions within the same project. In order to do this, we will use Spring profiles.

Spring Configurations Based on Profiles

The first step is to split the application configuration based on the desired profile. In order to illustrate the example, let’s define two profiles: profile1 and profile2. Then our configurations can be:

import lombok.extern.log4j.Log4j2;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Profile;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@Log4j2
@TestConfiguration
@Profile("profile1")
public class Profile1Config {

    @Bean
    public ExecutorService executorService() {
        log.info("Creating profile1 executor service");
        return Executors.newFixedThreadPool(2);
    }
}
import lombok.extern.log4j.Log4j2;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Profile;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@Log4j2
@TestConfiguration
@Profile("profile2")
public class Profile2Config {

    @Bean
    public ExecutorService executorService() {
        log.info("Creating profile2 executor service");
        return Executors.newFixedThreadPool(2);
    }
}

Additionally, we may have a common Spring configuration classes. We may use the SpringTestConfig.java from our previous setup.

Cucumber Test Suites Configuration

The suite configuration consists of two parts: Cucumber Spring context configuration and the Cucumber test runner configuration.

Cucumber Spring Context Configuration

The next step is to define the Cucumber Spring context configuration. We will create a separate package for each profile: com.example.cucumberconfig.profile1 and com.example.cucumberconfig.profile2. In each of these packages there will reside a Cucumber Spring config and a test runner.

The profile specific configs can be:

package com.example.cucumberconfig.profile1;

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

@CucumberContextConfiguration
@SpringBootTest(
        webEnvironment = SpringBootTest.WebEnvironment.NONE,
        classes = {SpringTestConfig.class, Profile1Config.class}
)
@ActiveProfiles("profile1")
public class CucumberSpringContextConfig {}
package com.example.cucumberconfig.profile2;

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

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

For each of the configuration we activate a specific profile (using @ActiveProfiles annotation) which filter out the necessary bean spawning and their injection.

Cucumber Test Runner Configuration

In order to define our Cucumber test runners, we need to figure out how to tell Cucumber to run a subset of our scenarios and to ignore the others. We could, of course, use Cucumber tags, but it is annoying to mark each our scenario with a tag. One solution would be to split our feature files into profile-specific folders and to link them to the suite using @SelectClasspathResource.

Another problem is defining profile specific steps. The solution to this would be to split the step definitions across profile specific classes and add them to GLUE path using @ConfigurationParameter. Additionally to step definitions, we need to glue the Cucumber Spring context configuration class. Fortunately, we can specify multiple packages in the GLUE path as comma-separated values.

Unfortunately, currently, Cucumber (7.2.2) does not have a glueClass feature of supporting dynamically adding the packages to GLUE path using the class name (there is a bounty for this feature, though). We have to enumerate the packages manually. This is error prone and inconvenient, especially when the name of the glued packages change.

The final test runner configurations are:

package com.example.cucumberconfig.profile1;

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/profile1")
@ConfigurationParameter(
        key = GLUE_PROPERTY_NAME,
        value = "com.example.bdd.custom," +
                "com.example.bdd.stepdefs.common," +
                "com.example.bdd.stepdefs.profile1," +
                "com.example.cucumberconfig.profile1"
)
@ConfigurationParameter(key = PLUGIN_PUBLISH_QUIET_PROPERTY_NAME, value = "true")
@ConfigurationParameter(key = FILTER_TAGS_PROPERTY_NAME, value = "@RunMe")
public class RunProfile1CucumberTests {}
package com.example.cucumberconfig.profile2;

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/profile2")
@ConfigurationParameter(
        key = GLUE_PROPERTY_NAME,
        value = "com.example.bdd.custom," +
                "com.example.bdd.stepdefs.common," +
                "com.example.bdd.stepdefs.profile2," +
                "com.example.cucumberconfig.profile2"
)
@ConfigurationParameter(key = PLUGIN_PUBLISH_QUIET_PROPERTY_NAME, value = "true")
@ConfigurationParameter(key = FILTER_TAGS_PROPERTY_NAME, value = "@RunMe")
public class RunProfile2CucumberTests {}

Step Definitions

Due to the magical Cucumber-Spring integration, if the step definition classes reside under GLUE path, then they are added to context and transformed into Spring beans. Thus, be careful what you glue and try to minimize your test application context. Moreover, by gluing all step definitions and having profile specific bean injection into some particular step definition classes may corrupt the application context during startup because of the missing beans. Always check the packages added to GLUE path in the test runner if any errors raise.

In our case, we will split our step definitions across packages so that we would be able to glue them. Additionally, we have some step definitions common to both profiles. Gluing them too.

Finally, our step definitions may look like this:

package com.example.bdd.stepdefs.common;

import com.example.bdd.custom.CustomLogger;
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 DatabaseStepDefinitions {

    private final CustomLogger customLogger;

    @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");
    }
}
package com.example.bdd.stepdefs.profile1;

import com.example.bdd.custom.CustomLogger;
import io.cucumber.java.en.Then;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.context.annotation.Profile;

import java.util.concurrent.ExecutorService;

@Log4j2
@RequiredArgsConstructor
@Profile("profile1")
public class Profile1StepDefinitions {

    private final ExecutorService executorService;

    private final CustomLogger customLogger;

    @Then("^A custom profile1 step is executed$")
    public void aCustomProfileStepIsExecuted() {
        log.info("Custom profile1 step execution");
    }
}
package com.example.bdd.stepdefs.profile2;

import io.cucumber.java.en.Then;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.context.annotation.Profile;

import java.util.concurrent.ExecutorService;

@Log4j2
@RequiredArgsConstructor
@Profile("profile2")
public class Profile2StepDefinitions {

    private final ExecutorService executorService;

    @Then("^A custom profile2 step is executed$")
    public void aCustomProfileStepIsExecuted() {
        log.info("Custom profile2 step execution");
    }
}

The injection of the executorService and customLogger beans in this case is done to illustrate the profile-specific dependency injection by Spring. Additionally, we have added a custom step for each of the profiles.

Feature File and Scenarios

The scenarios are split across feature files and subfolders which are loaded by the test runner. The common test scenarios can be located in a common subfolder which can be loaded as a separate resource to the test runner just like the profile specific feature files.

Finally, our feature files are as follows:

Feature: Test our service's rest endpoints for 'profile1'

  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
    And A custom profile1 step is executed
Feature: Test our service's rest endpoints for 'profile2'

  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
    And A custom profile2 step is executed

File Structure

The entire project file structure is as follows:

.
├── pom.xml
└── src
    └── test
        ├── java
        │   └── com
        │       └── example
        │           ├── bdd
        │           │   ├── custom
        │           │   │   └── CustomLogger.java
        │           │   └── stepdefs
        │           │       ├── common
        │           │       │   └── DatabaseStepDefinitions.java
        │           │       ├── profile1
        │           │       │   └── Profile1StepDefinitions.java
        │           │       └── profile2
        │           │           └── Profile2StepDefinitions.java
        │           ├── config
        │           │   ├── Profile1Config.java
        │           │   ├── Profile2Config.java
        │           │   └── SpringTestConfig.java
        │           └── cucumberconfig
        │               ├── profile1
        │               │   ├── CucumberSpringContextConfig.java
        │               │   └── RunProfile1CucumberTests.java
        │               └── profile2
        │                   ├── CucumberSpringContextConfig.java
        │                   └── RunProfile2CucumberTests.java
        └── resources
            ├── bdd
            │   ├── profile1
            │   │   └── accessibility.feature
            │   └── profile2
            │       └── accessibility.feature
            ├── cucumber.properties
            └── log4j2.xml

(Bonus) Maven Surefire Plugin Options

In order to configure the Cucumber pretty output when running the tests using Maven, here there are some options that might be handy:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>3.0.0-M5</version>
    <configuration>
        <properties>
            <configurationParameters>
                cucumber.junit-platform.naming-strategy=long
                cucumber.plugin=pretty,html:target/site/cucumber-pretty.html
                cucumber.publish.quiet=true
                cucumber.publish.enable=false
            </configurationParameters>
        </properties>
    </configuration>
</plugin>

More about Cucumber properties you can read here.

More about Maven Surefire plugin configuration you can read here.

Disadvantages of the Proposed Model

One disadvantage of the proposed model is the inconvenience to run a scenario from IntelliJ IDEA. Given that we have installed the Cucumber Java plugin for IntelliJ, when we try to run the Open and close a connection to database 1-1 scenario from bdd/profile1/accessibility.feature, then an exception is thrown stating that we have multiple classes which configure the Spring context. This is happening because the Cucumber Java plugin is auto-magically setting up the GLUE path for our project. Cucumber expects only one test framework setup per project (at least the plugin expects it) so in order to glue the right classes the plugin scans for all the classes annotated with @CucumberContextConfiguration and adds them to GLUE path. Thus, the test fails to start because we have multiple Spring contexts configured by multiple classes.

The solution for this, or rather inconvenience, is to edit the IntelliJ “Run configuration” and to delete the undesired packages from Glue input field. After this, rerun the scenario and it will pick the right context configuration. Check here how to edit the run configuration in IntelliJ.

References

  1. Cucumber Spring, https://github.com/cucumber/cucumber-jvm/tree/main/spring
  2. Cucumber JUnit Platform Engine, https://github.com/cucumber/cucumber-jvm/tree/main/junit-platform-engine
  3. Spring Profiles, https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.profiles
  4. Cucumber Tags, https://cucumber.io/docs/cucumber/api/?sbsearch=junit%205#tags
  5. Cucumber Configuration Options, https://github.com/cucumber/cucumber-jvm/tree/main/junit-platform-engine#configuration-options
  6. Maven Surfire JUnit 5, Configuration Parameters, https://maven.apache.org/surefire/maven-surefire-plugin/examples/junit-platform.html
  7. Edit “Run configuration” in IntelliJ IDEA, https://www.jetbrains.com/help/idea/run-debug-configuration.html