Automatically launch docker-compose from Gradle for dev and integration tests

As applications get more complex with many moving parts, integration tests against the full-stack are becoming more critical. In this article, I’ll show how to use docker-compose in a Spring Boot application with Neo4j backend to aid both the development and the integration tests.

This blog post assumes that you already know what integration tests are, how Gradle works. Spring Boot and Neo4j are just example components for the project. The code for this post is available on Github.

Creating fresh Spring Boot application.

I used Spring Initializer to generate the skeleton project. You can get your copy from here. Since this blog post is not focused on building Spring Boot applications, you can download the needed skeleton from Github or develop your own code. I also built some unit tests for the project, but this is out of scope for this blog post.

Building Integration Tests

To build integration tests, we need four things:

  1. Write the tests
  2. Configure Gradle to run them
  3. Configure Gradle to launch the backend automatically for the integration tests.
  4. Run the tests

Step 1: Writing the integration tests

spring:
neo4j:
uri: "bolt://localhost:8687"
authentication:
username: neo4j
password: password

Step 2: Configure Gradle to the integration tests

a. Defining new sourceSet in build.gradle:

sourceSets {
integrationTest {
java {
srcDir "src/integration/java"
compileClasspath += main.output + test.output
runtimeClasspath += main.output + test.output
}
resources.srcDir file("src/integration/resources")
}
}

b. Configuring the integration sourceSetin build.gradle:

We configure the integration tests source to inherit the configurations from implementation and testImplementation. Hence, all the dependencies will be available in the integration tests.

configurations {
compileOnly {
extendsFrom annotationProcessor
}
integrationTestImplementation.extendsFrom implementation
integrationTestImplementation.extendsFrom testImplementation
integrationTestRuntime.extendsFrom testRuntime
}

c. Defining an integration tests task in build.gradle:

Finally, we define a new task called integrationTest to run the integration tests. In this task, we use the JUnit platform to drive the tests and specify that integration tests should run after unit tests.

task integrationTest(type: Test) {
group 'Test'
useJUnitPlatform()
mustRunAfter test
testClassesDirs = sourceSets.integrationTest.output.classesDirs
classpath = sourceSets.integrationTest.runtimeClasspath
}

Step 3: Integrating docker-compose

a. Defining a docker-compose file in src/integration/resources/docker-compose.yml:

Note the specified exposed ports and credentials configured for Neo4j in the docker-compose file.

version: '3'
services:
neo4j:
image: neo4j:4.2.5
hostname: neo4j
container_name: neo4j
ports:
- "8474:7474"
- "8687:7687"
environment:
NEO4J_AUTH: neo4j/password
NEO4J_dbms_memory_heap_max__size: 256M
NEO4J_dbms_logs_debug_level: DEBUG

b. Configuring Gradle to run docker-compose for the integration tests:

For this purpose, I used com.avast.gradle.docker-compose plugin. It’s easy to configure. Just add it to the plugins in build.gradle:

plugins {
id 'org.springframework.boot' version '2.4.4'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
id 'idea'
id 'com.avast.gradle.docker-compose' version "0.14.2"
}

And then configure it to run for the integration tests:

dockerCompose {
integration {
// Define the docker compose file for integration testing
useComposeFiles = ["${buildDir}/resources/integrationTest/docker-compose.yml"]
isRequiredBy(integrationTest)
}
}

Step 4: Running the integration tests

You can aslo manually start/shutdown the containers with ./gradlew integrationComposeUp or ./gradlew integrationComposeDown.

Running docker-compose for dev

version: '3'
services:
neo4j:
image: neo4j:4.2.5
hostname: neo4j
container_name: neo4j
ports:
- "7474:7474"
- "7687:7687"
environment:
NEO4J_AUTH: neo4j/password
NEO4J_dbms_memory_heap_max__size: 256M
NEO4J_dbms_logs_debug_level: DEBUG

Then extended the dockerCompose config in build.gradle to:

dockerCompose {
dev {
// Specify that bootRun depends on dockerCompose
useComposeFiles = ["${buildDir}/resources/main/docker-compose.yml"]
isRequiredBy(bootRun)
}
integration {
// Define the docker compose file for integration testing
useComposeFiles = ["${buildDir}/resources/integrationTest/docker-compose.yml"]
isRequiredBy(integrationTest)
}
}

Now, i can run it manually with ./gradlew devComposeUp and ./gradlew devComposeDown.

Limitations and room for improvements

  1. Testcontainers provides an alternative approach to this method and integrates more into your java tests. I’ve not explored that project in detail yet.

Just another nomad in love with technology