Using Maven and Docker to Test a Spring Rest Service

2018-12-15

Tags:
  • java
  • spring
  • rest
  • docker
  • maven
Categories:
  • howto

Problem

You are developing a REST service and want to run acceptance tests against it, as if it were already installed.

Solution

A possible solution is to package the server as a Docker image, and run the acceptance tests against that image.

The server

For a Spring Boot application, use the Spring Boot Maven Plugin to build an executable fat jar so we only have one file to deploy.

<plugin>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-maven-plugin</artifactId>
	<version>${spring.boot.version}</version>
	<executions>
		<execution>
			<goals>
				<goal>repackage</goal>
			</goals>
		</execution>
	</executions>
</plugin>

During the package phase, we create a Docker image with fabric8's excellent Docker Maven plugin.

The image is defined as follows:

<plugin>
	<groupId>io.fabric8</groupId>
	<artifactId>docker-maven-plugin</artifactId>
	<configuration>
		<images>
			<image>
				<alias>dockerest</alias>
				<name>cosminul/dockerest:latest</name>

				<build>
					<from>openjdk:8-jdk-alpine</from>
					<assembly>
						<targetDir>/</targetDir>
						<descriptor>dockerest/docker-assembly.xml</descriptor>
					</assembly>
					<entryPoint>
						<exec>
							<arg>java</arg>
							<arg>-jar</arg>
							<arg>/opt/dockerest/${project.artifactId}-${project.version}.jar</arg>
						</exec>
					</entryPoint>
				</build>
			</image>
		</images>
	</configuration>
	<executions>
		<execution>
			<id>build</id>
			<phase>package</phase>
			<goals>
				<goal>build</goal>
			</goals>
		</execution>
	</executions>
</plugin>

This is how the assembly file looks like, for copying the generated jar from Maven's target directory to a directory in the Docker image (in this case, /opt/dockerest):

<?xml version="1.0" encoding="utf-8"?>
<assembly
	xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3 http://maven.apache.org/xsd/assembly-1.1.3.xsd">
	<files>
		<file>
			<source>${project.build.directory}/${project.artifactId}-${project.version}.jar</source>
			<outputDirectory>opt/dockerest</outputDirectory>
			<fileMode>0755</fileMode>
		</file>
	</files>
</assembly>

The client

The client is the acceptance test. It may be a separate Maven module.

We can use the docker-maven-plugin again, and run the previously built Docker image during the integration-test phase. To be precise, we start the container during pre-integration-test and stop it during post-integration-test.

The container might need a few seconds to start, and the application might also not be ready instantly. We need to make sure the tests don't start until the environment is ready. If our application runs on port 8080, we can wait for it to be available. In case something breaks during startup (so the port never becomes available), we also need a timeout (e.g. 20,000 milliseconds = 20 seconds):

<wait>
	<tcp>
		<ports>
			<port>8080</port>
		</ports>
	</tcp>
	<time>20000</time>
</wait>

Furthermore, the port 8080 is internal to the container, but it needs to be mapped to a specific port in the host environment. We could map it to a hardcoded predefined port, but that might not be available in the user's environment. Instead, we let Docker choose a random port, and just map that value to a Maven property that we can make available to our tests. In this case, we choose the name tomcat.port for the Maven property.

<ports>
	<port>tomcat.port:8080</port>
</ports>

Here's the complete plugin configuration:

<plugin>
	<groupId>io.fabric8</groupId>
	<artifactId>docker-maven-plugin</artifactId>
	<configuration>
		<images>
			<image>
				<alias>dockerest</alias>
				<name>cosminul/dockerest:latest</name>

				<run>
					<namingStrategy>alias</namingStrategy>
					<ports>
						<!-- The "tomcat.port" maven-property will be set by this plugin 
							with the actual port used by the container. -->
						<port>tomcat.port:8080</port>
					</ports>
					<wait>
						<tcp>
							<ports>
								<port>8080</port>
							</ports>
						</tcp>
						<time>20000</time>
					</wait>
				</run>
			</image>
		</images>
	</configuration>
	<executions>
		<execution>
			<id>start</id>
			<phase>pre-integration-test</phase>
			<goals>
				<goal>start</goal>
			</goals>
		</execution>
		<execution>
			<id>stop</id>
			<phase>post-integration-test</phase>
			<goals>
				<goal>stop</goal>
			</goals>
		</execution>
	</executions>
</plugin>

In order to make the port available to the test code, we can configure the Maven Failsafe Plugin (which runs our integration and acceptance tests) to map the maven property (defined by the Docker Maven plugin) to a system property:

<plugin>
	<groupId>org.apache.maven.plugins</groupId>
	<artifactId>maven-failsafe-plugin</artifactId>
	<configuration>
		<systemPropertyVariables>
			<webPort>${tomcat.port}</webPort>
		</systemPropertyVariables>
	</configuration>
</plugin>

Then in the Java code we can access the value with System.getProperty:

int port = Integer.parseInt(System.getProperty("webPort"));

Show me the code

There's a complete example for the solution described above.

Sources

Building a RESTful Web Service with Spring
https://spring.io/guides/gs/rest-service/
Maven Lifecycle Phases
https://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html