DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Last call! Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • Keep Your Application Secrets Secret
  • Using KRaft Kafka for Development and Kubernetes Deployment
  • Setting Up Local Kafka Container for Spring Boot Application
  • Leveraging Test Containers With Docker for Efficient Unit Testing

Trending

  • Contextual AI Integration for Agile Product Teams
  • How to Format Articles for DZone
  • Unlocking the Benefits of a Private API in AWS API Gateway
  • Docker Base Images Demystified: A Practical Guide
  1. DZone
  2. Software Design and Architecture
  3. Containers
  4. Configure Testcontainers in Spring Boot 3.x, 2.x, and Reactive

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

By 
Seun Matt user avatar
Seun Matt
DZone Core CORE ·
Mar. 10, 25 · Tutorial
Likes (3)
Comment
Save
Tweet
Share
3.3K Views

Join the DZone community and get the full member experience.

Join For Free

Overview

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:

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:

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:

XML
 
<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:

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

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:

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

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:

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:

Java
 
@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:

  1. Create a testcontainers.properties in src/test/resources
  2. Add hub.image.name.prefix = private.docker-repo.io/ where docker.io should be the URL of your private Docker repo or mirror site
  3. 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


Docker (software) Spring Boot Testing

Published at DZone with permission of Seun Matt. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Keep Your Application Secrets Secret
  • Using KRaft Kafka for Development and Kubernetes Deployment
  • Setting Up Local Kafka Container for Spring Boot Application
  • Leveraging Test Containers With Docker for Efficient Unit Testing

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!