Configure Testcontainers in Spring Boot 3.x, 2.x, and Reactive
Testcontainers allows you to spin up lightweight, disposable containers for databases, messaging systems, and more, ensuring your tests are isolated and predictable
Join the DZone community and get the full member experience.
Join For FreeOverview
Testcontainers provides disposable Docker containers for databases, message queues, Redis, and so much more. It enables us to run fully integrated SpringBoot tests without mocking the Database, Redis, and even RabbitMQ interactions.
In this tutorial, we will learn how to set up Testcontainers in a Spring Boot application. This approach will work for Spring Boot version 3.x, 2.x, and even reactive Spring Boot applications.
Project Setup and Dependency Installation
For this article, we will work with a reactive Spring Boot application that manages a Pet
resource. It uses the PostgreSQL database and runs a database schema on startup.
The demo application includes two integration tests for the endpoints:
PetControllerIntegrationTest.java
:
@Test
void givenValidRequestBody_whenCreate_thenReturn2XX() {
PetRequest request = new PetRequest();
request.setName("Aja Ode " + insecure().randomAlphabetic(5).toUpperCase());
request.setColour("orange");
webTestClient.post().uri(Routes.Pets.PETS)
.bodyValue(request)
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$.data.name").isEqualTo(request.getName())
.jsonPath("$.data.colour").isEqualTo(request.getColour());
}
@Test
void givenExistingPets_whenGetAll_thenReturnAllPets() {
Pet pet = new Pet();
pet.setName("Blimey the Goat " + insecure().randomAlphabetic(5).toUpperCase());
pet.setColour("red");
StepVerifier.create(petRepository.save(pet))
.assertNext(Assertions::assertNotNull)
.verifyComplete();
webTestClient.get().uri(Routes.Pets.PETS)
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$..name").value(Matchers.hasItem(pet.getName()));
}
For these tests to run successfully, we need a running PostgreSQL database server that's accessible to the application.
Using Postgres on my local machine means the tests will not be able to run successfully in a CI/CD pipeline. Moreover, we do not want to mix test data with real application data. Therefore, we will add Testcontainers as part of the test setup, so it can run anywhere, independently without any mocking.
We will use the testcontainers
BOM (Bill Of Material) to manage the various versions of the dependencies:
pom.xml
:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-bom</artifactId>
<version>1.20.4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
After that, we can add the following dependencies:
<dependencies>
<!-- other dependencies omitted for brevity -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
The PostgreSQL driver in the test
scope is for Testcontainers to be able to run the init
schema, as we will soon learn.
Testcontainers Configuration
The number one requirement for Testcontainers is a running Docker environment. So, ensure you have Docker on your local machine and that it is running.
The test classes, for the demo application, have been organized such that a class named BaseSpringBootTest
houses the annotations and main configurations. Every other test class will extend this one.
We will create a static instance of the PostgreSQLContainer
in the BaseSpringBootTest
class.
BaseSpringBootTest.java
:
static PostgreSQLContainer<?> postgresContainer =
new PostgreSQLContainer<>("postgres:latest")
.withDatabaseName("testcontainers_demo")
.withInitScript("init.sql")
.withUsername("test")
.withExposedPorts(5432)
.withPassword("test");
The PostgreSQLContainer
instance above, is set to use the username and password test
and run on port 5432
- the default.
The string "postgres:latest
" passed to the constructor is the specific Docker image we want to use. With this, we can control the specific version of the PostgreSQL database we want to use for the application.
Testcontainers support running an init script when initializing a database container. In the above listing, with the method .withInitScript("init.sql")
, the PostgreSQLContainer
will run all SQL statements in a file named init.sql
.
The file should be in the src/test/resources
directory.
Now, we need to start the container and replace the Spring database connection properties, with that of the PostgreSQLContainer
.
We can achieve this in Spring Boot via a @DynamicPropertySource
annotated method. This method enables us to replace application properties during start up.
Listing 3.2 BaseSpringBootTest.java
@DynamicPropertySource
static void registerDynamicProperties(DynamicPropertyRegistry registry) {
postgresContainer.start();
registry.add("spring.r2dbc.url", () -> String.format(
"r2dbc:postgresql://%s:%d/%s",
postgresContainer.getHost(),
postgresContainer.getMappedPort(5432),
postgresContainer.getDatabaseName()
));
registry.add("spring.r2dbc.username", postgresContainer::getUsername);
registry.add("spring.r2dbc.password", postgresContainer::getPassword);
Runtime.getRuntime().addShutdownHook(new Thread(postgresContainer::stop));
}
In the registerDynamicProperties
method, we first invoke the start()
method of the PostgreSQLContainer
. This will cause the program flow to wait for a complete start before proceeding with other steps.
Once the container has started, we then add the Spring datasource properties to the DynamicPropertyRegistry
with values from the recently started container. All database interactions will now be using the PostgreSQL running in the Docker container we started.
Finally, we registered a shutdown hook to stop the container once the application run is complete. In this case, once the test run is completed. Notice how we used spring.r2dbc.url
and not spring.datasource.url
. This is because the demo application is reactive and uses the r2dbc
library for database interactions.
We can achieve the same for a non-reactive Spring Boot application:
BaseSpringBootTest.java
:
@DynamicPropertySource
static void registerDynamicProperties(DynamicPropertyRegistry registry) {
postgresContainer.start();
registry.add("spring.datasource.url", postgresContainer::getJdbcUrl);
registry.add("spring.datasource.username", postgresContainer::getUsername);
registry.add("spring.datasource.password", postgresContainer::getPassword);
Runtime.getRuntime().addShutdownHook(new Thread(postgresContainer::stop));
}
Testcontainers, by default, use random ports. For example, the default PostgreSQL port is 5432
. PostgreSQLContainer
will internally run on 5432
but map it to a random port for accessibility.
In order for us to get the mapped port for 5432
, we can use the function postgresContainer.getMappedPort(5432)
.
The complete BaseSpringBootTest.java
is like this:
Listing 3.4 BaseSpringBootTest.java
@SpringBootTest
@ContextConfiguration(classes = WebTestClientConfiguration.class)
public class BaseSpringBootTest {
static PostgreSQLContainer<?> postgresContainer =
new PostgreSQLContainer<>("postgres:latest")
.withDatabaseName("testcontainers_demo")
.withInitScript("init.sql")
.withUsername("test")
.withExposedPorts(5432)
.withPassword("test");
@DynamicPropertySource
static void registerDynamicProperties(DynamicPropertyRegistry registry) {
postgresContainer.start();
registry.add("spring.r2dbc.url", () -> String.format(
"r2dbc:postgresql://%s:%d/%s",
postgresContainer.getHost(),
postgresContainer.getMappedPort(5432),
postgresContainer.getDatabaseName()
));
registry.add("spring.r2dbc.username", postgresContainer::getUsername);
registry.add("spring.r2dbc.password", postgresContainer::getPassword);
Runtime.getRuntime().addShutdownHook(new Thread(postgresContainer::stop));
}
}
At this point, we can now run our tests without mocking the database or relying on a local database instance.
Whenever we start the test run. Testcontainers will pull the required Docker image from Docker Hub, and create a Docker container for the application to use. When the test run completes, Testcontainers will dispose of the Docker container automatically.
Generic Containers
Testcontainers provide containers for other resources like RabbitMQ, MySQL, CassandraSQL, Local Stack, and so much more. We simply need to add the required modules to our applications as needed.
Moreover, Testcontainers has a generic container that we can use to start up a container with any Docker image. For example, we can create an instance of a Generic container for Redis:
BaseSpringBootTest.java
:
static GenericContainer redisContainer = new GenericContainer<>("redis:latest")
.withExposedPorts(6379);
Using the same technique as above, we can then start the container in a DynamicPropertySource
method and override the application's Redis connection properties:
@DynamicPropertySource
static void registerDynamicProperties(DynamicPropertyRegistry registry) {
postgresContainer.start();
redisContainer.start();
registry.add("spring.r2dbc.url", () -> String.format(
"r2dbc:postgresql://%s:%d/%s",
postgresContainer.getHost(),
postgresContainer.getMappedPort(5432),
postgresContainer.getDatabaseName()
));
registry.add("spring.r2dbc.username", postgresContainer::getUsername);
registry.add("spring.r2dbc.password", postgresContainer::getPassword);
registry.add("spring.data.redis.host", redisContainer::getHost);
registry.add("spring.data.redis.port", () -> redisContainer.getMappedPort(6379));
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
postgresContainer.stop();
redisContainer.stop();
}));
}
Following this configuration (on lines 5, 16, 17, and 21), all Redis interactions within the Spring Boot test will be using the Redis instance provided by the Testcontainers.
Tips and Tricks
1. Connecting to a Running TestContainer Instance
Sometimes, we may want to connect to the temporary Docker containers before they're disposed of. The trick is to add a breakpoint somewhere in the test logic and run the test in debug mode.
Once the execution is paused, search the console output for a string like jdbc:
. You will see a line like Container is started (JDBC URL: jdbc:postgresql://localhost:53321/testcontainers_demo?loggerLevel=OFF)
.
With this host and port, we can connect to the running database using the default username and password test
.
2. Using a Private Docker Repository
Can you use private Docker repositories? Sure. Especially to circumvent the rate limit on Docker Hub. To achieve this, we need to do the following:
- Create a
testcontainers.properties
insrc/test/resources
- Add
hub.image.name.prefix = private.docker-repo.io/
wheredocker.io
should be the URL of your private Docker repo or mirror site - Remember to authenticate to the private Docker repo by executing
docker login private.docker-repo.io -u username -p $DOCKER_PASS
Every time Testcontainers needs to pull an image, it will use the configured private Docker repo as opposed to the default Docker Hub.
3. Running the Test in a CI/CD Runner
Just like your local machine, your CI/CD environment must also have a valid Docker environment for the tests to run. You can confirm this by running docker --version
in your pipeline step.
Different CI/CD providers have different approaches to availing a Docker environment within the pipeline. GitLab CI/CD will call this feature Docker-in-Docker. Do ensure to consult your CI/CD provider's documentation on achieving the same.
The demo project uses CircleCI, and enabling Docker in the pipeline is as simple as adding a setup_remote_docker
step.
Conclusion
Testcontainers is revolutionary. It eliminates the complexities around running an integrated test for a Spring Boot application.
The complete source code is available on GitHub. Consult the Testcontainers official documentation for more info.
Happy coding!
You can watch a video version of this article below.
Video
Published at DZone with permission of Seun Matt. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments