Docker with Gradle: Integration testing using containers

testing spock build docker container gradle ci travis


February 18, 2018

In the first blog post on "Docker with Gradle" you learned how to package a Spring Boot application as a Docker image. After verifying that the image works as expected you pushed the image to a registry. Being able to produce and push a new image of an application with every single commit lays the foundation for enabling supplemental automation workflows.

Integration testing plays an important role in the software development lifecycle to ensure functional and non-functional requirements have been met. With the rise of microservices, we see an increasing number of projects that reach out to other services e.g. by performing a HTTP call. During integration testing, external services and endpoints need to be available. It can be very tedious, time-consuming and error-prone to bring up multiple services just for the purpose of integration testing especially during development. Docker containers enable standing up services with a desired state on demand.

In this blog post, you will learn how to pull a specific tag of a Docker image from a registry and use it as fixture for integration testing. You will also automate the process as part of a Continuous Integration job on Travis CI. You can find the full source code used on GitHub.


Setting up integration testing

The code under test in this example builds upon the account management application created in an earlier blog post. DefaultAccountManager.java implements a method for retrieving account information by ID via HTTP(s) and crediting a monetary amount to the existing balance of that account. The test DefaultAccountManagerIntegrationTest.groovy requires the account management service to be up and running to verify the integration point.

It’s considered good practice to separate the source code of different test types with the help of dedicated directories. Furthermore, different types of tests should be runnable individually by invoking corresponding tasks. For example sometimes you might want to just run unit tests, other times will want to run integration tests.

Listing 1 demonstrates how to create a dedicated source set and Test task for the purpose of integration testing. The logic has been extracted into a script plugin named integration-test.gradle. Keeping the setup for integration testing separate from the main build script improves maintainability and readability.

gradle/integration-test.gradle

sourceSets {
    integrationTest {
        groovy.srcDir file('src/integrationTest/groovy')
        resources.srcDir file('src/integrationTest/resources')
        compileClasspath += sourceSets.main.output + configurations.testRuntime
        runtimeClasspath += output + compileClasspath
    }
}

task integrationTest(type: Test) {
    description = 'Runs the integration tests.'
    group = 'verification'
    testClassesDirs = sourceSets.integrationTest.output.classesDirs
    classpath = sourceSets.integrationTest.runtimeClasspath
    mustRunAfter test
}

check.dependsOn integrationTest

Listing 1. Creating an integration test source set and task

The build.gradle file in turn applies the integration test script plugin as shown below.

build.gradle

apply from: 'gradle/integration-test.gradle'

Listing 2. Applying the integration test script plugin

With this configuration in place, any source code under src/integTest/groovy can be compiled and executed by running the integrationTest task from the command line.

In the course of the next sections, you will set up the Docker plugin to retrieve the Docker image bundling the account management application. Moreoever, you will add tasks for starting and stopping a Docker container as fixture for integration testing.


Configuring the Docker plugin

The Docker plugin knows how to do the heavy lifting of communicating with Docker from the Gradle build. It’s binary artifact is available on the Gradle plugin portal. Because of a known limitation of applying third-party plugins from a script plugin, the Docker plugin needs to be applied by type as shown in listing 3.

gradle/integration-test.gradle

buildscript {
    repositories {
        maven {
            url 'https://plugins.gradle.org/m2/'
        }
    }
    dependencies {
        classpath 'com.bmuschko:gradle-docker-plugin:3.2.3'
    }
}

apply plugin: com.bmuschko.gradle.docker.DockerRemoteApiPlugin

Listing 3. Applying the Docker plugin

Next, you will use a custom task type provided by the plugin to pull an image from a registry.


Pulling the image from a registry

By default the Docker plugin tries to resolve images from Docker Hub. Thankfully, the image of the account management application is available from bmuschko/account-web-service so no additional setup is required. For more information on using private registries, refer to plugin documentation. The following code snippet creates a task of type DockerPullImage for pulling the tag 1.0.0 of the image from Docker Hub.

gradle/integration-test.gradle

import com.bmuschko.gradle.docker.tasks.image.DockerPullImage

task pullImage(type: DockerPullImage) {
    repository = 'bmuschko/account-web-service'
    tag = '1.0.0'
}

Listing 4. Pulling the Docker image for a specific tag

Downloading the image might take a while depending on your network bandwidth. The compressed size of the image is roughly 71 MB. The following section explains how to start and stop a container running the image.


Starting and stopping the container

Starting and stopping the container is existential for successfully executing integration tests. Below you can find the detailed steps for starting the container:

  1. Creating a container from the image with an exposed port binding.

  2. Starting up the container.

  3. Waiting until the application running within the container becomes accessible.

Step 3 is extremely important to ensure that the integration tests do not start executing before the application can serve requests. The task startAndWaitOnHealthyContainer inspects the starting container periodically and only proceeds if the health check returns a "ready state". It also implements a circuit breaker pattern in case the operation takes longer than expected.

gradle/integration-test.gradle

import com.bmuschko.gradle.docker.tasks.container.DockerCreateContainer
import com.bmuschko.gradle.docker.tasks.container.DockerStartContainer
import com.bmuschko.gradle.docker.tasks.container.DockerStopContainer
import com.bmuschko.gradle.docker.tasks.container.extras.DockerWaitHealthyContainer

task createContainer(type: DockerCreateContainer) {
    dependsOn pullImage
    targetImageId { pullImage.getImageId() }
    portBindings = ['8080:8080']
}

task startContainer(type: DockerStartContainer) {
    dependsOn createContainer
    targetContainerId { createContainer.getContainerId() }
}

task startAndWaitOnHealthyContainer(type: DockerWaitHealthyContainer) {
    dependsOn startContainer
    timeout = 60
    targetContainerId { createContainer.getContainerId() }
}

task stopContainer(type: DockerStopContainer) {
    targetContainerId { createContainer.getContainerId() }
}

Listing 5. Creating tasks for starting and stopping a container

Finally it’s time to hook the container tasks into the task for executing integration tests.


Using the container fixture for integration testing

Integrations tests may or may not fail. The container running the account management application needs to be teared down independent of the outcome of the test execution. Dangling but unused services accumlating over time lead to unnecessary resource consumption and will inevitability overload a system.

Meet the Gradle API method Task.finalizedBy(Object…​). It behaves analogous to Java’s try/finally construct and will execute the finalizer task even if a failure occurs.

gradle/integration-test.gradle

integrationTest {
    dependsOn startAndWaitOnHealthyContainer
    finalizedBy stopContainer
}

Listing 6. Modeling the integration test fixture setup

The workflow is now fully set up and ready for use. Running integration tests as part of a deployment pipeline ensures that every commit is automatically verified.


Performing integration testing on Travis CI

Most organizations adopting Continuous Delivery will want to run integration tests as part of the pipeline. Implementing the integration test step with Travis CI is straightforward. To use Docker as part of your CI job, you will need to first enable the service.

As seen in the previous code examples, the integration test fixture logic is completely encapsulated in the build script. What that means is that you can just call off to the integrationTest task without having to configure any additional Docker-related steps as part of the CI definition. The approach will make it extremly simple to switch CI products if needed.

.travis.yml

language: java
install: true
sudo: required

services:
  - docker

jdk:
  - oraclejdk8

script:
  - ./gradlew integrationTest -s

before_cache:
  - rm -f  $HOME/.gradle/caches/modules-2/modules-2.lock
  - rm -fr $HOME/.gradle/caches/*/plugin-resolution/

cache:
  directories:
    - $HOME/.gradle/caches/
    - $HOME/.gradle/wrapper/

Listing 7. Executing the integration test workflow on Travis CI



comments powered by Disqus