How To Run Docker Compose with Testcontainers

Codeal
3 min readMar 11, 2021

In our daily jobs we often need to implement integration tests that will validate the feature implemented, in an environment as close as possible to production environment, if you consider the technologies that you are working with.

One of the most common cases is to startup a database docker container during the tests execution and to do this we can list two options:

  • Start docker compose with Gradle and run the tests, as you can see in our previous post the example how to do it — the link you can find here.
  • Manage the container during the test execution by starting the database before the tests, running the tests and after finish destroying the container.

To have the control of the container lifecycle within the test framework, lets make a project with Testcontainers.

Requirements This article requires a previous knowledge in JUnit, Gradle and Docker

Testcontainers

Testcontainers is 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.

The project has integration with JUnit 4, Junit 5 and Spock, allowing it to be integrated with our test suite.

Testcontainers has a modular implementation that allow us to include in our project specific containers implementations or work with generic containers and docker-compose by default.

From the list of modules, we have database containers that can be included one by one, ElasticSearch, LocalStack container and more.

These modules are specific implementation classes for a specific docker container version, for example, PostgreSQLContainer today works with the default version 9.6.12, but you can change this version as a parameter.

The library allows us to create our own class implementation for a specific container, extending the GenericContainer, making easy to manage custom or private docker containers.

Setup project

Step 1 — Create a docker-compose file

The process is quite simple, first let us create a docker-compose.yml file, that we will use to setup our containers.

Why docker-compose?

Because it is more flexible and we can manage multiple containers at the same time if needed, using the power of docker-compose.

So, we have the docker-compose file including a PostgreSQL database.

version: '3'
services:
postgres:
image: postgres:13.2-alpine
environment:
POSTGRES_USER: codeal
POSTGRES_PASSWORD: codeal
POSTGRES_DB: codeal
volumes:
- ./init.sql:/docker-entrypoint-initdb.d/01-init.sql

The init.sql script is optional, we use in our example to create the table during startup of the container.

Step 2: Add Gradle dependencies

testImplementation("org.testcontainers:testcontainers:1.15.2")
testImplementation("org.testcontainers:junit-jupiter:1.15.2")
testImplementation("org.junit.jupiter:junit-jupiter-api:5.4.2")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.4.2")

Step 3: Write the test

The first thing is to define the container, that in our case we will use the module DockerComposeContainer.

@Testcontainers
public class SimpleIntegrationTest {
@Container
public static DockerComposeContainer dockerComposeContainer =
new DockerComposeContainer(new File("src/test/resources/docker-compose-test.yml"))
.withExposedService("postgres", 5432);
@Test
@DisplayName("Should find Book by id")
public void shouldFindBookByIdTest() {
final var testBook = bookRepository.findById(1);

Assertions.assertTrue(testBook.isPresent());
final var book = testBook.get();
Assertions.assertNotNull(book.getId());
Assertions.assertEquals(book.getTitle(), "Test containers");
Assertions.assertEquals(book.getAuthor(), "Codeal.IO");
Assertions.assertEquals(book.getYear(), 2021);
}
}

The annotation @TestContainers is the integration with Junit Jupiter that allow us to start and stop containers automatically, the integration will look for all containers annotated with @Container, start them before tests and stop them after the tests.

Done!

We have Testcontainers starting containers using docker-compose, running the test with JUnit and stopping the container after the test finishes.

How can I access the container?

In our example, when we are defining the DockerComposeContainer, we say that we want to expose a service with a port, this does not mean that the port 5432 for example is free for us to use.

To access this container, we need to use functions from Testcontainers to obtain the host and port for the service that we decided to expose.

final var host = dockerComposeContainer.getServiceHost("postgres", 5432);
final var port = dockerComposeContainer.getServicePort("postgres", 5432);

Next Steps: Improvements

Local Compose

Use the local docker compose from host machine that is running the tests, this will increase the speed of execution for your tests.

new DockerComposeContainer(new File("src/test/resources/docker-compose-test.yml"))
.withLocalCompose(true)

Timeout on startup

Define the timeout to startup the container before run your tests.

.withExposedService("postgres", 5432,        Wait.forListeningPort().withStartupTimeout(Duration.ofSeconds(30)));

That’s all folks!

Thanks for reading and feel free to add your comment with any questions that you have, we will be really happy to help!

The sample project you can find in our GitHub.

--

--