Integration testing with Docker-Compose, Gradle and Spring Boot
This post has been featured on This Week in Spring – November 28th, 2017.
Lately I have been preparing a small project called simple-meetup that I plan to use for different purposes. You’ll find the repository at GitHub: github.com/michael-simons/simple-meetup. It’s the first project where I used Gradle very intensive and I like my build file a lot.
The first intention of this repository is being an example for an article for OBJEKTSpektrum on testing. The project used JUnit 5 at the beginning, but I switched back to 4 for various reasons, which I explain in a follow up. Thanks upfront to Christian Stein, JUnit 5 committer and contributor, for your interest and feedback on this!
I already wrote about integration testing with Docker and Maven. In that post I used the docker-maven-plugin to fire up supporting services for my tests.
While researching similar options to use with Gradle, I stumbled upon this post by Thomas Kieffer from Codecentric. He describes the docker-compose-role in detail. This rule is a JUnit 4 rule designed to start containers defined in a docker-compose.yml before tests and tearing them down afterwards and also waiting for services to become available before running tests.
This is quite nice, as it doesn’t make your tests dependend on the build tool itself. There is activity to make this available as JUnit 5 extension as well, see #138.
While Thomas demonstrated a generic approach, I’d like to show you how to use this with Spring Boot and replace Spring Boot properties at the right point before the Spring context gets refreshed.
For my meetup project, I need a database and I want to run integration tests against the “real deal”. Here’s my Docker-Compose file:
And this is what the integration tests looks like:
@RunWith(SpringRunner.class) @ActiveProfiles("it") @DataJpaTest @ContextConfiguration(initializers = PortMappingInitializer.class) public class EventRepositoryIT { private static DockerComposeRule docker = DockerComposeRule.builder() .file("src/integrationTest/resources/docker-compose.yml") .waitingForService("it-database", HealthChecks.toHaveAllPortsOpen()) .waitingForService("it-database", PostgresHealthChecks::canConnectTo) .build(); @ClassRule public static TestRule exposePortMappings = RuleChain.outerRule(docker) .around(new PropagateDockerRule(docker)); // Actual tests omitted } |
Two things to notice: First I didn’t use the DockerComposeRule
as a class rule. Instead, I used a chain of JUnit rules. A rule chain works from outer to inner rules. The declaration public static TestRule exposePortMappings = RuleChain.outerRule(docker).around(new PropagateDockerRule(docker))
says: Apply the docker-compose-rule first and then an instance of PropagateDockerRule
. Second: I apply ContextConfiguration
with an instance of Springs ApplicationContextInitializer
, dubbed PortMappingInitializer
.
The initializers are called just before the Spring Boot context gets refreshed and autoconfiguration kicks in. What my initializer does is getting the instance of the docker compose rule out of a thread local and reads the name of all services and the mapped ports from it:
The important part is the usage of `TestPropertySourceUtils`. I use it to add inline properties to the environment. Those test properties have higher priority than any other properties except the devtools properties.
The purpose of the PropagateDockerRule
is then plain and simple: It stuffs the current instance of the docker compose rule into the thread local:
That way, I can use inlined properties like this: spring.datasource.url = jdbc:postgresql://localhost:${it-database.port}/postgres
. Spring Boot and it’s starters use it to point my database agains the instance I just started. The project repo includes additional health checks, but that’s all there is. The test database would also be subject to initialization, wether one uses Flyway, Liquibase or a simple schema or data script.
What’s nice about the setup is that I can fire up the test from my IDE, regardless of the build-tool. While one can usually run single Unit tests from the IDEs out there, its more complicated to run a single failsafe-based integration test with Maven.