Skip to main content

Testcontainers in Spring Boot Integration Tests

Spring Testcontainers Testing
Author
Harpal Singh
Software Engineer
Table of Contents

1. Introduction

In this tutorial, we’ll check out how to use testcontainers in a Spring boot application when creating integration tests.

Testcontainers are lightweight, disposable instances of infrastructure services such as databases, message brokers, etc., that can run in Docker containers. When writing integration tests we would like to have these services available instead of mocking them or handling the setup and teardown using custom error-prone scripts.

Let’s see how we can set up a database container using testcontainers.

2. Prerequisites

First, we need to set up our project to work with testcontainers. We can download and install Docker from the official website: https://www.docker.com/get-started

Next, we need to add the testcontainers dependencies in our pom.xml file:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-testcontainers</artifactId>
</dependency>
<dependency
    <groupId>org.testcontainers</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>1.19.7</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>postgresql</artifactId>
    <version>1.19.7</version>
    <scope>test</scope>
</dependency>

Make sure to get the latest version of the testcontainers junit-jupiter and testcontainers-postgresql dependencies. Also, make sure to use the correct version for spring-boot-testcontainers, as some classes might not be available in the older version of Spring Boot.

3. Using Testcontainers in Spring Boot Tests

Spring Boot has included support for Testcontainers for a while now. However, with the release of Spring Boot 3.1, the integration has been improved further. Let’s see how the old approach compares to the new one.

In versions before Spring Boot 3.1, we had to manually configure Spring Boot to connect to the services running inside the containers. Which often leads to including some boilerplate code involving using the @DynamicPropertySource annotation to set the necessary properties:

@SpringBootTest
@Testcontainers
class MyIntegrationTests {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16");

    @Test
    void myTest() {
        // ...
    }

    @DynamicPropertySource
    static void postgresProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }
}

Above, we annotated the test with @Testcontainer to let the test class know that it uses testcontainers. We then defined an instance of the PostgreSQL container using the @Container annotation. This will spin up a PostgreSQL container with the specified version. Now, usually we would have configured the connection properties inside the application.properties file. But since we are using testcontainers, we don’t know yet what the connection properties will be. So we use @DynamicPropertySource to retrieve the connection properties from the container instance and set them in the Spring Boot application context.

As we can see, this approach involves a few moving parts and is very verbose even for a simple test.

Starting with Spring Boot 3.1, we can use the @ServiceConnection annotation to simplify the process:

@SpringBootTest
@Testcontainers
class MyIntegrationTests {

    @Container
    @ServiceConnection
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16");

    @Test
    void myTest() {
        // ...
    }
}

Here, we have replaced the @DynamicPropertySource annotation with @ServiceConnection. This removes the need to remember all the properties for any given service and lets Spring Boot handle the connection configuration automatically. In detail, it works by discovering the type of container and creating a corresponding ConnectionDetails bean. Then, Spring Boot uses this to configure the connection to the service running in the Testcontainer.

We can check all the supported containers in the official Spring Boot documentation.

4. Using Testcontainers During Development

Let’s go a step further to see how we can use Testcontainers during development using new features in Spring Boot 3.1.

Historically, we used to use different profiles to switch between the development and test environments. But now, there is a better way to manage this and get the development environment as similar as possible to the production environment.

Instead of creating a test profile, we can create a new main method for running our application locally using testcontainers:

public class TestMyApplication {
    public static void main(String[] args) {
        SpringApplication.from(MyApplication::main).run(args);
    }
}

We must create this class inside the test root package to keep the testcontainers dependencies only in the test scope. Otherwise, the dependencies get included in full jar files, which bloats the application size and it isn’t used in production.

Now, we need to configure the test container for PostgreSQL and let the new main method know about it. First, we create a configuration file and annotate it with @TestConfiguration:

@TestConfiguration(proxyBeanMethods = false)
class MyContainersConfiguration {
    @Bean
    @ServiceConnection
    PostgreSQLContainer<?> postgresContainer() {
        return new PostgreSQLContainer<>("postgres:16");
    }
}

In short, we are just creating a bean that represents the container instance of PostgreSQL. Again, we annotate it with @ServiceConnection to let Spring Boot know that it should manage the lifecycle as well as configure it to use with our application.

Now, we update the test-main method:

public class TestMyApplication {
    public static void main(String[] args) {
        SpringApplication.from(MyApplication::main)
            .with(MyContainersConfiguration.class)
            .run(args);
    }
}

Finally, we can run the test-main method to start the application using Maven:

./mvnw spring-boot:test-run

Or using Gradle:

./gradlew bootTestRun

This will start the application and the containers will automatically start up and shut down with the application. As we can see, the maven and gradle plugins already know about the test-main method and will start the application using that.

5. Saving the Container Data Between Restarts

With the configuration above, we are creating a new instance of the container every time the application is started. Therefore, we lose all their data. However, we might want to save the state of the container between restarts. To do this, we can use the @RestartScope annotation from Spring Boot DevTools on the container bean methods:

@Bean
@ServiceConnection
@RestartScope
PostgreSQLContainer<?> postgresContainer() {
    return new PostgreSQLContainer<>("postgres:16");
}

Alternatively, we can use the experimental reusable containers feature in Testcontainers by calling withReuse(true) on the container:

new PostgreSQLContainer<>("postgres:16").withReuse(true);

This will save the data between restarts. Also, this speeds up the application startup time as we don’t spin a new container each time.

6. Conclusion

In this article, we have seen how to use Testcontainers in Spring Boot integration tests. We have seen how to set up a database container using Testcontainers and how to use the @ServiceConnection annotation to simplify the process. We have also seen how to use Testcontainers during development using the new features in Spring Boot 3.1.

Related

Returning HTTP 4XX Errors in a Spring Application
Spring HTTP
Get Values Defined in Properties File in Spring
Spring Properties Basics
Configuring the Port for a Spring Boot Application
Spring Basics