Testing your Docker containers with TestContainers and Groovy

tl;dr: https://github.com/bsideup/testing-docker-images-with-testcontainers

My favorite part about DevOps is the ability to apply my Software development knowledge when I do some infrastructure related tasks.

Given that knowledge, can we make testing of infrastructure-as-code as easy as testing application code?


BASH? It’s too old-school!

Usually, when we mention “infrastructure” and “Software development” in the same sentence, we mean Ruby, Python or Golang (depending on your age, beard size, or the amount of coffee you drink at Starbucks).

However, how about Java and the whole JVM world?
Hold on, hold on! I know what you’re thinking right now. It is not slow until you make it so ;)
And don’t worry, the only thing that’s need to be installed on your machine is Docker.


Meet TestContainers

Java is known for the huge amount of testing libraries, and I happen to be a member of TestContainers — a Java library that supports JUnit tests, providing lightweight, throwaway instances of common databases, Selenium web browsers, or anything else that can run in a Docker container.

Once I thought — why not test my Nginx configuration with it?
But wait… It is a Java library, and I do not want to configure my infra project as a Java project, right? I want to… Script it!

And guess what? I also, among other things, happen to be a committer of Apache Groovy — one of the best JVM scripting languages!


Our test

Say we have a config for Nginx:

And we want to test that our rewrite magic does the thing and redirects from e.g. http://example.com/en/blog to http://en.example.com/blog.

To start, we’re going to create a Groovy file named test.groovy.
One of my favorite features of Groovy is Grapes — the ability to automatically grab the dependencies of your script!
I’m looking at you, pip/gem/whatever 😉

Let’s Grab the dependencies first:

Here we depend on testcontainers-groovy-script and say that we want to inherit from TestcontainersScript to avoid some boilerplate when we use TestContainers from Groovy script.

Now we declare the Docker container we want to test:

As you can see, we use an official Nginx image from the Docker Hub, map config file into it and expose port 80. Expose? What does it mean? It’s one of the features of TestContainers — the ability to map ports randomly to avoid port conflicts with the host.

Above is more or less an equivalent of:

docker run -v default.conf:/etc/nginx/conf.d/default.conf -p 80 \
nginx:1.9.4

Remember I said that JVM has a lot of cool testing libs? One of them is Rest-Assured — a very simple yet powerful library to make the assertions about your http APIs. We’re going to use it to make the calls on our Nginx server and verify the responses:


Running the test

I’m not that naive to ask you to install JVM & Groovy on your sysops machine (Althought it’s just brew install groovy & groovy test.groovy ).

We’re going to use Docker… to run our Docker tests! Inception, anyone?

To avoid typing a long “docker run” command, we will use Docker Compose:

Here we use an official Apache Groovy image, mount grapes’ cache folder, and the most important part — mount the Docker socket, so that TestContainers will be able to communicate with the Docker daemon.

Now, if we run the command, the output should have something like:

$ docker-compose run --rm tests
[main] INFO org.testcontainers.dockerclient.EnvironmentAndSystemPropertyClientProviderStrategy - Found docker client settings from environment
[main] INFO org.testcontainers.dockerclient.DockerClientProviderStrategy - Found Docker environment with Environment variables, system properties and defaults. Resolved:
dockerHost=unix:///var/run/docker.sock
apiVersion='{UNKNOWN_VERSION}'
registryUrl='https://index.docker.io/v1/'
registryUsername='root'
registryPassword='null'
registryEmail='null'
dockerConfig='DefaultDockerClientConfig[dockerHost=unix:///var/run/docker.sock,registryUsername=root,registryPassword=<null>,registryEmail=<null>,registryUrl=https://index.docker.io/v1/,dockerConfigPath=/root/.docker,sslConfig=<null>,apiVersion={UNKNOWN_VERSION},dockerConfig=<null>]'
[main] INFO org.testcontainers.DockerClientFactory - Docker host IP address is 172.17.0.1
[main] INFO org.testcontainers.DockerClientFactory - Connected to docker:
Server Version: 18.03.1-ce
API Version: 1.37
Operating System: Docker for Mac
Total Memory: 4948 MB
[main] INFO org.testcontainers.DockerClientFactory - Ryuk started - will monitor and terminate Testcontainers containers on JVM exit
ℹ︎ Checking the system...
✔ Docker version should be at least 1.6.0
[main] INFO 🐳 [nginx:1.9.4] - Creating container for image: nginx:1.9.4
[main] INFO 🐳 [nginx:1.9.4] - Starting container with ID: 4228481881c3473bb81e57102a4d1718d070c20a6aba85120ca696c518a58445
[main] INFO 🐳 [nginx:1.9.4] - Container nginx:1.9.4 is starting: 4228481881c3473bb81e57102a4d1718d070c20a6aba85120ca696c518a58445
[main] INFO 🐳 [nginx:1.9.4] - Container nginx:1.9.4 started
..
Time: 2.498
OK (2 tests)

As you can see, TestContainers successfully found Docker, verified that it is configured correctly and executed our tests.

Conclusion

Writing tests is fun… when it’s fun :D And thanks to the libraries like TestContainers, JVM languages like Groovy, and technologies like Docker, we can make it so!

Links