How to run end-to-end tests in a Maven Multi-Module project?

João Paulo Gomes
WAES
Published in
9 min readJun 1, 2023

Introduction

This article will introduce how to write end-to-end tests for your microservices using Gherkin, Cucumber, and Kotlin. We will do it using a multi-module Maven project and configure Maven to start our Spring application before executing the tests. This is a practical article with a clear goal of showing how to make all the pieces work together smoothly. This way, you can run your tests locally with the same command they will run in your pipeline.

1. Creating a Maven multi-module Kotlin Spring application

In a multi-module Maven application, you can have only one place to define all the dependencies and plugin versions, the parent POM. It makes it easier to update them. Besides that, you can put things not directly part of your application in different modules, like stubs of other applications and integration tests. If you want to use the hexagonal architecture, it’s also an excellent way to split your application.

Creating the parent POM

  1. Create a folder called maven-cucumber-tests
  2. Create a new file called pom.xml inside this folder with this content:
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.5</version>
<relativePath/>
</parent>

<groupId>com.johnowl</groupId>
<artifactId>maven-cucumber-tests</artifactId>
<version>0.0.1-SNAPSHOT</version>

<packaging>pom</packaging>

<properties>
<java.version>17</java.version>
<kotlin.version>1.7.22</kotlin.version>
</properties>

<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
<configuration>
<args>
<arg>-Xjsr305=strict</arg>
</args>
</configuration>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-allopen</artifactId>
<version>${kotlin.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</project>

This file declares Spring Boot as its parent POM and adds the Kotlin dependencies. The packaging type is pom instead of jar because this is the parent POM.

Adding the app module

1. Create a folder called app and add a pom.xml file in this folder with this content:

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>com.johnowl</groupId>
<artifactId>maven-cucumber-tests</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>

<groupId>com.example</groupId>
<artifactId>app</artifactId>
<version>0.0.1-SNAPSHOT</version>

<packaging>jar</packaging>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-kotlin</artifactId>
</dependency>
<dependency>
<groupId>io.projectreactor.kotlin</groupId>
<artifactId>reactor-kotlin-extensions</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-reactor</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>

<plugins>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<configuration>
<compilerPlugins>
<plugin>spring</plugin>
</compilerPlugins>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

This pom.xml file declared the parent POM as its parent, it adds some Spring dependencies and configures some plugins.

2. Inside the app folder, add a folder src/main/kotlin/com/johnowl/appwith a file Application.kt with this content:

package com.johnowl.app

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController

@SpringBootApplication
class Application

fun main(args: Array<String>) {
runApplication<Application>(*args)
}

@RestController
class HelloController {

@GetMapping("/hello")
fun hello(): String = "Hello world!"

}

This is our application, an API that returns the “Hello world!” message.

3. Add the reference to the module in the parent POM. Go to the file maven-cucumber-tests/pom.xml and add this content after the </properties> tag:

<modules>
<module>app</module>
</modules>

4. In the same file, add the spring-boot-maven-plugin in the /project/build tag:

<pluginManagement>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</pluginManagement>

At this point, you should be able to run your project. Run the command mvn -f app/pom.xml spring-boot:run and navigate to http://localhost:8080/hello to check if it works.

2. Adding a new module for end-to-end tests

1. In the root directory of your project, create a folder called tests and add a pom.xml file with this content:

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

<parent>
<groupId>com.johnowl</groupId>
<artifactId>maven-cucumber-tests</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>

<groupId>com.example</groupId>
<artifactId>tests</artifactId>
<version>0.0.1-SNAPSHOT</version>

<dependencies>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-java8</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</dependency>

<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
</build>

</project>

This file also uses the parent POM as its parent, and it adds some dependencies that we need to create the end-to-end tests.

2. Add the reference to the new module in the parent POM. The list of modules should be like this:

    <modules>
<module>app</module>
<module>tests</module>
</modules>

If you try to compile your application now, it will fail. At the beginning of this article, it’s written the parent POM is the place to centralize all your dependency versions. We added some dependencies in the new module that are not there yet. Let’s fix it.

3. Add the test dependencies in the parent POM managed dependencies. Open the parent POM and add this content after the </dependencies> tag:

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

<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>${commons-logging.version}</version>
<scope>compile</scope>
</dependency>
<!-- cucumber end -->

<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<version>${rest-assured.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>

The version numbers are stored in the properties of the POM, so you also need to add the version numbers there. This is a good approach to have all the versions in the same place and also to make them reusable. Add the properties below inside the properties tag:

<commons-logging.version>1.2</commons-logging.version>
<cucumber-bom.version>7.11.2</cucumber-bom.version>
<junit-jupiter.version>5.9.2</junit-jupiter.version>
<rest-assured.version>5.3.0</rest-assured.version>

After doing this, your application should compile and run successfully again.

3. Writing a test scenario with Gherkin

Gherkin is a language used to describe your tests. It’s a structured and very readable way to declare your tests. Even non-technical people are capable of reading it. Let’s create our test scenario!

Create a folder called tests/src/test/resources/features and add a file called hello_tests.feature inside it. If you are using the IntelliJ IDE, make sure that you have the Gherkin Plugin installed.

Open the file and add this content:

Feature: Hello

Scenario: Say hello
Given I call the "/hello" endpoint with the "GET" method
Then I receive the message "Hello world!"

This is the structure of a Gherkin test. First, we have the feature name, and then we can have one or more scenarios. The most basic scenario structure is the one that follows:

Given <I do something>
Then <I expect something as a result>

In our test scenario, we want to call an API and receive the “Hello world!” message as a result. It would be nice if we could stop here, but the Cucumber framework is not smart enough to transform this text into code. So we need to implement all the steps. Let’s do that using Kotlin and Rest-Assured.

4. Implement the test steps using Kotlin and Rest Assured

If you try to run the test using the IntelliJ green icon like the one in the image below:

Test written using Gherkin on IntelliJ IDE

The test will fail, and you will receive an error message like this:

Step undefined
You can implement this step and 1 other step(s) using the snippet(s) below:

Given("I call the {string} endpoint with the {string} method", (String string, String string2) -> {
// Write code here that turns the phrase above into concrete actions
throw new io.cucumber.java8.PendingException();
});
Then("I receive the message {string}", (String string) -> {
// Write code here that turns the phrase above into concrete actions
throw new io.cucumber.java8.PendingException();
});


Step skipped

We are going to write something similar but using Kotlin instead of Java. Create a new folder tests/src/test/kotlin and add a file inside it called StepDefinitions.kt with this content:

package tests

import io.cucumber.java8.En
import io.restassured.RestAssured.given
import io.restassured.response.Response
import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.MatcherAssert.assertThat

class StepDefinitions : En {

init {
val restAssured = given().baseUri("http://localhost:8080")
lateinit var response: Response

Given("I call the {string} endpoint with the {string} method") { path: String, method: String ->
response = restAssured.request(method, path)
}

Then("I receive the message {string}") { expectedMessage: String ->
assertThat(response.body.asString(), equalTo(expectedMessage))
}
}
}

The StepDefinitions class inherits from En, and the steps are implemented inside the init block. All the text with double quotes is transformed into parameters we can use in the step. It makes it easier to reuse steps in other scenarios.

If you start the application and then rerun the test using your IDE, it should work as shown in the image below:

End-to-end tests succeeded

5. Running the tests with Maven

If you run the tests using the command mvn clean verify and analyze the logs, you’ll see that the end-to-end tests are not running. To fix this issue, we need to add another file inside the folder tests/src/test/kotlin. Create a file called CucumberIT.kt in this folder, and use this content for this file:

package tests

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

@RunWith(Cucumber::class)
@CucumberOptions(
features = ["src/test/resources/features"]
)
class CucumberIT

This is the entry point class to run the Cucumber tests. The Failsafe plugin uses the suffix IT to identify this class as a test.

Now, we need to add the Maven Failsafe Plugin to run the tests. Because integration tests are slower them unit tests, it’s wise not to run them every time, only when we really want it. To achieve this requirement, we can use Maven profiles. Let’s do that, open the file /tests/pom.xml and add the content below right after the </build> tag:

<profiles>
<profile>
<id>end-to-end-tests</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
</plugin>
</plugins>
</build>
</profile>
</profiles>

This configuration tells Maven to run this plugin only when the profile “end-to-end-tests” is active. To specify a profile in the Maven command line, you can use the parameter -P, as you can see in the example that follows:

mvn clean verify -Pend-to-end-tests

After running this command, you will see an error message like this:

Step failed
java.net.ConnectException: Connection refused: connect

This happens because the test is trying to connect to our application, but the application is not running. To fix that, we must tell Maven to start the application before executing the end-to-end test.

6. Run the Kotlin Spring application automatically before the end-to-end test

To run the application before the end-to-end test, we will use a Maven plugin called process-exec-maven-plugin. The first step is adding the plugin to the plugin management section in the parent POM. You can add it right after the Spring Boot plugin, as you can see in the code below:

[...]
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>com.bazaarvoice.maven.plugins</groupId>
<artifactId>process-exec-maven-plugin</artifactId>
<version>${process-exec-maven-plugin.version}</version>
</plugin>
</plugins>
</pluginManagement>
[...]

You must also add a new property in the same file with the plugin version number.

<process-exec-maven-plugin.version>0.7</process-exec-maven-plugin.version>

The last step is to configure the plugin to start the application, open the file tests/pom.xml and add the code below with the existing plugin un the end-to-end-tests profile.

<plugin>
<groupId>com.bazaarvoice.maven.plugins</groupId>
<artifactId>process-exec-maven-plugin</artifactId>
<executions>
<execution>
<id>app</id>
<phase>pre-integration-test</phase>
<goals>
<goal>start</goal>
</goals>
<configuration>
<name>app</name>
<workingDir>app</workingDir>
<waitForInterrupt>false</waitForInterrupt>
<healthcheckUrl>http://localhost:8080/hello</healthcheckUrl>
<arguments>
<argument>java</argument>
<argument>-jar</argument>
<argument>${basedir}/../app/target/app-${project.version}.jar</argument>
</arguments>
</configuration>
</execution>
<execution>
<id>stop-all</id>
<phase>post-integration-test</phase>
<goals>
<goal>stop-all</goal>
</goals>
</execution>
</executions>
</plugin>

This plugin allows you to configure one or more applications that should be started before your tests. It will start them according to the order they are declared. The last execution step is to stop all the applications started. They will be stopped in the reverse order they started.

To allow the plugin to know if your application is ready to receive traffic, you need to configure a health check URL.

Conclusion

As demonstrated in this article, it’s possible to have integration tests running in a Maven multi-module project. The nicest thing is that you can run your tests locally using the same command used in the pipeline.

You can also see the complete example project.

Do you think you have what it takes to be one of us?

At WAES, we are always looking for the best developers and data engineers to help Dutch companies succeed. If you are interested in becoming a part of our team and moving to The Netherlands, look at our open positions here.

WAES publication

Our content creators constantly create new articles about software development, lifestyle, and WAES. So make sure to follow us on Medium to learn more.

Also, make sure to follow us on our social media:
LinkedInInstagramTwitterYouTube

--

--

João Paulo Gomes
WAES
Writer for

Hi! I’m JP! I work as a Kotlin and Java developer and in my spare time I like to cook. My github https://github.com/johnowl