Using docker for integration/e2e-tests
November 22, 2015
I wanted to write up on a technology that has exploded in popularity the last couple of years; Docker! I’m gonna give a brief example on how you can build your own Docker image, start a container from an existing image and finally how to integrate these containers and run an integration/end-to-end-test on them. Everything is built through our good ol’ Maven.
Prereq
Before we start, let’s make sure you have:
- Maven
- Docker (If you are an OSX/Windows-user I would suggest you have a look at Docker Toolbox).
- Protractor globally installed (npm install -g protractor)
Please note that the plugin we are going to use is using the docker remote API through a TCP-socket. If you are running on linux, make sure the shell that you use to perform the maven-build has the DOCKER_HOST environment variable set to a TCP-socket and not a unix-socket (Bind Docker to another host/port or a Unix socket).
All the code that is discussed below can be found at https://github.com/olbpetersson/docker-e2e-example
Let’s containerize our fat-jar
The baseline for this little project was https://github.com/olbpetersson/spring-boot-reactive, a minor system which included a fat-jar using spring-boot which was connected to a MongoDB. Given that, let’s take a look on how we could produce an image which will launch our fat-jar.
src/main/docker/spring-boot/Dockerfile
FROM java:8
COPY docker-e2e-example-0.0.1-SNAPSHOT.jar /reactive.jar
RUN bash -c 'touch /reactive.jar'
EXPOSE 8080
ENTRYPOINT ["java"]
CMD ["-jar", "/reactive.jar"]
So, we know that we are going to launch a jar-file, which obviously makes us having a dependency towards java. What you see in the first line, FROM java:8, is that we are basing our new image on an already existing one. These existing images can be found on the Docker Hub Repository. Something to be aware of is that these images are pretty general and are seldom optimized for size (the Java-image is about 650 MB). You could write up your own image from scratch and post it to your own Docker Image Repository (which is really easy to do since a docker-hub is also a docker-image), however that is nothing I will go into this time.
Further on we just copy our built jar into the root of the docker-container and make sure that we expose the port 8080 to the outside world (this will be a bit clearer once we go through the mvn-build).
A thing that is a bit unintuitive here is the RUN BASH -c 'touch /reactive.jar'
. This is needed because when we perform the COPY, the jar is placed on disk in an unmodified state. Performing this touch will give us a “last modified”-state on the file which is needed to execute the java -jar in the ENTRYPOINT/CMD.
Using existing images and running containers together
In the reactive-example I used MongoDB as-is. Therefore, there is no benefit in creating our own mongoDB-image but rather use an existing one. This is as simple as doing
docker run mongo
. This will first try to find a mongo image locally, and if not it will look for it in the docker hub and perform a docker pull mongo:latest
before it actually starts the container with the run command.
What docker does is that it sets up it’s own virtual network (you should be able to see a ethernet adapter named docker0 in your ifconfig). If you want to reach your containered docker from outside your engine, you will need to do a portforwarding using docker run -p 27017:27017 mongo
. Other good parameters to know is the -d which starts the container as a daemon, or -ti which starts the container in an interactive mode. However, if you want your containers to communicate between eachother, you can use dockers network and link your images together. Below is an example of how you link two containers
docker run --name mongodb -d mongo && docker run --link mongodb:mongodb -p 8080:8080 "your-application"
.
This will make sure that the “your-application”-container knows about the mongo under the hostname ‘mongodb’. You will see that I use this technique in the maven section
And then I cheated a bit with the final frontend test
Here I took a shortcut and used a really dumb protractor test which just tries to go to the url and verifies that it’s actually able to reach it. I also didn’t put the time into configuring node/npm to install protractor in build time (which is why you have to have protractor installed). When we have a look at the maven build you will see that I cheat by calling a shell-script. Since this post is more about the concept of using docker I wont put any time into fixing these quirks but be aware that if you are running on windows you’ll have to make some manual steps to get it to work.
I want to mention that there are several plugins in maven to both install npm/node and to execute protractor, either directly or via Grunt. If you are interested in these parts I recommend having a look at https://github.com/eirslett/frontend-maven-plugin
Tying it all together with maven
In our final step we are going to do this all with maven which is supported in all of the big CI/build-tools that exists. The outline of what we will do in maven is:
Given a profile 'docker-e2e-test' (mvn clean install -Pdocker-e2e-test)
:
- Creates a docker image based on our jar-artifact
- Starts the created docker image together with a mongo-container
- Runs the protractor test to make sure that it all works together
To follow along the next section, I propose you open up this pom.xml.
Create the docker image
So first of in the package phase, we want to copy over the jar in our target-directory and trigger a build of a new image based on our Dockerfile (see above). This is done by:
<plugin>
...
<executions>
<execution>
...
<goals>
<goal>build-images</goal>
</goals>
<configuration>
<images>
<image>
<id>${docker.image.prefix}-spring-boot/${project.artifactId}</id>
<dockerFile>${project.basedir}/src/main/
docker/spring-boot/Dockerfile</dockerFile>
<artifacts>
<artifact>
<file>${project.build.directory}/
${project.artifactId}-${project.version}.jar</file>
</artifact>
</artifacts>
</image>
</images>
</configuration>
...
Starting the containers
So before we start the test, we want to make sure that everythings is prepared. In the pre-integration-test maven phase we start the spring-boot application and the mongodb by:
<execution>
<phase>pre-integration-test</phase>
<id>start</id>
<goals>
<goal>start-containers</goal>
</goals>
<configuration>
<forceCleanup>true</forceCleanup>
<containers>
<container>
<id>mongodb</id>
<image>mongo:3.2</image>
<hostname>mongodb</hostname>
<waitForStartup>waiting for connections on port 27017</waitForStartup>
</container>
<container>
<id>spring-boot</id>
<image>
${docker.image.prefix}-spring-boot/${project.artifactId}
</image>
<hostname>
${docker.image.prefix}-spring-boot/${project.artifactId}
</hostname>
<links>
<link>
<containerId>mongodb</containerId>
<containerAlias>mongodburl</containerAlias>
</link>
</links>
<waitForStartup>Started ReactiveApplication in</waitForStartup>
</container>
</containers>
</configuration>
</execution>
In the first container-tag we specify which image we want to use (mongo:3.2) which is equal to docker pull mongo:3.2, then we give it the alias mongodb (equal to –name mongodb) and then the wouterd-plugin offers this really neat feature with the waitForStartup-tag. What it does is that it regexps the expression within the tag and does not continue the maven build until it can find that expression in the docker logs of that container (basically stdout, you can see this by typing docker logs “container”).
The second container, our spring-boot-application, is started based on the id we gave it in the package-phase. Here the waitForStartup gives us a lot more benefit since we don’t want to start the protractor test until our application is up and running. Here you also see that we link our mongodb with the hostname mongodburl. Therefore, we need to update our application.properties
/src/main/resources/application.properties
spring.data.mongodb.host=mongodburl
spring.data.mongodb.port=27017
Running the test
Given my disclaimer of all the naughty things I did regarding the frontend test, here I just want to high light a few small things:
By default, this maven plugin port forwards all exposed ports (e.g. EXPOSE 8080 as in our Dockerfile). However the port that it will use on your host will be randomly selected. However, in the start-phase there are two maven-properties set for the host and the port, namely:
{docker.containers.<app>.ports.<port>/tcp.host}
{docker.containers.<app>.ports.<port>/tcp.port}
This is what I use when launching our frontend test
<plugin>
<artifactId>exec-maven-plugin</artifactId>
<groupId>org.codehaus.mojo</groupId>
<executions>
<execution>
<id>Start protractor</id>
<phase>integration-test</phase>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<executable>src/main/scripts/run_protractor.sh</executable>
<arguments>
<argument>
--params.host=${docker.containers.spring-boot.ports.8080/tcp.port}
--params.port=${docker.containers.spring-boot.ports.8080/tcp.port}
</argument>
</arguments>
</configuration>
</execution>
</executions>
</plugin>
Finally, some tips, tricks and pitfalls
If you are completely new to docker there are a few commands that will help you out a lot when trying to figure out what happens:
To stop all running containers and remove them:
docker stop $(docker ps -a -q) && docker rm $(docker ps -a -q)
to remove all images:
docker rmi -f $(docker images -q)
to execute bash in a running docker:
docker exec -ti "container" bash
In my experience, building docker images through maven can give you a lot of benefits in both your development and in your pipelines. However, sometimes maven has problems interpreting the error codes that happens within the linux-container. Therefore I suggest that you always add
<execution>
<phase>verify</phase>
<id>verify</id>
<goals>
<goal>verify</goal>
</goals>
</execution>
In your pom. But if you see weird/unexpected things, read through the maven build log carefully as it sometimes may say Succeeded even though something happened within the container.
If you have any feedback on this post, don’t be afraid to hit me up on twitter!
Over and out
Ola