Skip to main content

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

“Clone & Run” Developer Experience

Gone are the days where we had to maintain a document with a long list of manual steps we needed to perform to set up the application locally before running the application. With Docker installing the application dependencies become easier. However, you still had to maintain different versions of scripts, based on your Operating System, to spin up the application dependencies as docker containers manually.

With the Testcontainers support added in Spring Boot 3.1.0, now developers can 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 are new to Testcontainers then please 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
}

Starting with Spring Boot 3.1.0, there is a new concept of ServiceConnection, which automatically configures the necessary Spring Boot properties for the supporting containers.

First add the spring-boot-testcontainers as 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 just adding @ServiceConnection without having to explicitly configure spring.datasource.url, spring.datasource.username and spring.datasource.password using @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 are 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, Neo4j etc. 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 in your integration tests.

For example, let’s say you are using Postgres and Kafka in your application. Then you can 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"));
   }
}

Then you can import the ContainersConfig in your tests as follows:

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

   //your tests
}

How to use a containers that doesn’t have ServiceConnection support?

In your applications, you may need to use a dependency for which there might not be a dedicated Testcontainers module or there is no 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. Then 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 have seen, we can use any containerized service and register the application properties.

Local development using Testcontainers

In the previous section, we have seen 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 for running the application locally.

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 have used the configuration class ContainersConfig using .with(...) to attach it to the application launcher.

Now you can run TestApplication from your IDE which will automatically start all the containers defined in ContainersConfig and automatically 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 have seen how to use Testcontainers for local development, but one challenge with this setup is every time application is modified and a build is triggered, the existing containers will be destroyed and new containers will be created. This may bring 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 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 build is triggered (Eclipse automatically triggers build when the code changes are saved, while in Intellij IDEA you need to trigger Build manually), the application will be restarted but uses the existing containers.

Conclusion

Modern software development involves using lots of technologies and tools to tackle the complex business needs which increased the development environment setup complexity. Improving the Developer Experience (DX) is not a “nice to have” anymore and it became a “necessity” to be agile.

We strongly believe that improving the developer experience will help the developers to do the right thing which in turn greatly improves the overall productivity.

Spring Boot 3.1.0 improved the Developer Experience (DX) by adding 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.

There are improvements not only for testing but also for local development. This significantly improves the developer experience by bringing the clone & run philosophy into reality.