Spring Boot Application Testing and Development with Testcontainers

Spring Boot 3.1.0 introduced great support for Testcontainers that’ll not only make writing integration tests easier, but also make local development a breeze.

Spring boot application testing & development with testcontainers

“Clone & Run” Developer Experience

Gone are the days of maintaining a document with a long list of manual steps needed to set up an application locally before running it. With Docker installing the application, dependencies became easier. But you still had to maintain different versions of scripts based on your Operating System, to manually spin up the application dependencies as Docker containers.

With the Testcontainers support added in Spring Boot 3.1.0, developers can now simply clone the repository and run the application! All the application dependencies, such as databases, message brokers, etc. can be configured to automatically start when we run the application.

If you’re new to Testcontainers, go through Getting started with Testcontainers in a Java Spring Boot Project guide to learn how to test your Spring Boot applications using Testcontainers.

Simplified integration testing using ServiceConnections

Prior to Spring Boot 3.1.0, we had to use @DynamicPropertySource to set the dynamic properties obtained from containers started by Testcontainers as follows:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
class CustomerControllerTest {

   @Container
   static PostgreSQLContainer<?> postgres = 
                  new PostgreSQLContainer<>("postgres:15-alpine");

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

   // your tests
}

Then, Spring Boot 3.1.0 introduced the new concept of ServiceConnection. This automatically configures the necessary Spring Boot properties for the supporting containers.

First, add the spring-boot-testcontainers as a test dependency:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-testcontainers</artifactId>
    <scope>test</scope>
</dependency>

Now, we can rewrite the previous example by adding @ServiceConnection without having to explicitly configure spring.datasource.url, spring.datasource.username, and spring.datasource.password using the @DynamicPropertySource approach.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
class CustomerControllerTest {

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

   // your tests
}

Notice that we’re not registering the datasource properties explicitly anymore.

The @ServiceConnection support not only works for relational databases but also many other commonly used dependencies like Kafka, RabbitMQ, Redis, MongoDB, ElasticSearch, and Neo4j. For the complete list of supporting services, see the official documentation.

You can also define all your container dependencies in one TestConfiguration class and import it into your integration tests.

For example, let’s say you’re using Postgres and Kafka in your application. You can then create a class called ContainersConfig as follows:

@TestConfiguration(proxyBeanMethods = false)
public class ContainersConfig {

   @Bean
   @ServiceConnection
   public PostgreSQLContainer<?> postgreSQLContainer() {
       return new PostgreSQLContainer<>("postgres:15.2-alpine");
   }

   @Bean
   @ServiceConnection
   public KafkaContainer kafkaContainer() {
       return new KafkaContainer(
                   DockerImageName.parse("confluentinc/cp-kafka:7.2.1"));
   }
}

Finally, you can import the ContainersConfig into your tests as follows:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Import(ContainersConfig.class)
class ApplicationTests {

   //your tests
}

How to use a container that doesn’t have ServiceConnection support

In your applications, you may need to use a dependency that doesn’t have a dedicated Testcontainers module or out-of-the-box ServiceConnection support from Spring Boot. Don’t worry, you can still use Testcontainers GenericContainer and register the properties using DynamicPropertyRegistry.

For example, you might want to use Mailhog for testing email functionality. In this case, you can use Testcontainers GenericContainer and register Spring Boot email properties as follows:

@TestConfiguration(proxyBeanMethods = false)
public class ContainersConfig {

   @Bean
   @ServiceConnection
   public PostgreSQLContainer<?> postgreSQLContainer() {
       return new PostgreSQLContainer<>("postgres:15.2-alpine");
   }

   @Bean
   @ServiceConnection
   public KafkaContainer kafkaContainer() {
       return new KafkaContainer(
                    DockerImageName.parse("confluentinc/cp-kafka:7.2.1"));
   }

   @Bean
   public GenericContainer mailhogContainer(DynamicPropertyRegistry registry) {
       GenericContainer container = new GenericContainer("mailhog/mailhog")
                                            .withExposedPorts(1025);
       registry.add("spring.mail.host", container::getHost);
       registry.add("spring.mail.port", container::getFirstMappedPort);
       return container;
   }
}

As we’ve seen, we can use any containerized service and register the application properties.

Local development using Testcontainers

In the previous section, we learned how to use Testcontainers for testing Spring Boot applications. With Spring Boot 3.1.0 Testcontainers support, we can also use Testcontainers during the development time to run the application locally.

To do this, create a TestApplication class in the test classpath under src/test/java as follows:

import org.springframework.boot.SpringApplication;

public class TestApplication {
   public static void main(String[] args) {
       SpringApplication
         .from(Application::main) //Application is main entrypoint class
         .with(ContainersConfig.class)
         .run(args);
   }
}

Observe that we’ve used the configuration class ContainersConfig using .with(...) to attach it to the application launcher.

Now you can run TestApplication from your IDE. It will automatically start all the containers defined in ContainersConfig and configure the properties.

You can also run TestApplication using the Maven or Gradle build tools as follows:

./mvnw spring-boot:test-run //Maven
./gradlew bootTestRun //Gradle

Using DevTools with Testcontainers at development time

We’ve now learned how to use Testcontainers for local development. But one challenge with this setup is that every time the application is modified and a build is triggered, the existing containers will be destroyed and new containers will be created. This can result in slowness or loss of data between application restarts.

Spring Boot provides devtools to improve the developer experience by refreshing the application upon code changes. We can use @RestartScope annotation provided by devtools to indicate certain beans to be reused instead of recreating them.

First, let’s add the spring-boot-devtools dependency as follows:

<!-- For Maven -->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-devtools</artifactId>
   <scope>runtime</scope>
   <optional>true</optional>
</dependency>

<!-- For Gradle -->
testImplementation "org.springframework.boot:spring-boot-devtools"

Now, add @RestartScope annotation on bean definitions in ContainersConfig as follows:

@TestConfiguration(proxyBeanMethods = false)
public class ContainersConfig {

   @Bean
   @ServiceConnection
   @RestartScope
   public PostgreSQLContainer<?> postgreSQLContainer() {
       return new PostgreSQLContainer<>("postgres:15.2-alpine");
   }

   @Bean
   @ServiceConnection
   @RestartScope
   public KafkaContainer kafkaContainer() {
       return new KafkaContainer(
                DockerImageName.parse("confluentinc/cp-kafka:7.2.1"));
   }

   ...
}

Now if you make any application code changes and the build is triggered, the application will restart but use the existing containers.

Please note: Eclipse automatically triggers a build when the code changes are saved, while in IntelliJ IDEA, you need to trigger a build manually.

Conclusion

Modern software development involves using lots of technologies and tools to tackle growing business needs. This has resulted in a significant increase in the complexity of the development environment setup. Improving the developer experience isn’t a nice to have anymore — it’s a necessity.

To improve this developer experience, Spring Boot 3.1.0 added out-of-the-box support for Testcontainers. Spring Boot and Testcontainers integration works seamlessly with your local Docker, on CI, and with Testcontainers Cloud too.

This is an impactful transformation for not only testing but also local development. And developers can now look forward to an experience that brings the clone & run philosophy into reality.

Learn more