2018-12-15
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:
- based on the standard OpenJDK Docker image
- with Java 8
- the Alpine version, to minimize the size
- with an added jar file, as described in an assembly file.
- executing the jar file (containing our service) on startup
<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