Dieser Artikel ist auch auf Deutsch verfügbar

Last year in November, the most recent major release of Spring Boot, version 3.0, appeared after five years. This means that the free support for the last still supported branch of Spring Boot 2, namely 2.7, runs out in November of this year. At the same time, the free support for version 3.0 is also coming to an end, making the most recently released version 3.1 the only version with free support.

The key aspects of Spring Boot 3.0 were the support for newer Java versions (at least Java 17 must now be used) and the updating of Java EE to Jakarta EE and the associated changes to the package names. A great many other dependencies were also updated. In particular, the upgrade to Spring Security 6 generally requires a number of changes, for which the Migration Guide can be quite helpful. Of course, there were also many other small changes and improvements to existing functionality.

In particular, Spring Boot 3.1 contains better and more direct integration of Testcontainers (see also my last column). The new support for Docker Compose during local development is also worth noting.

We would therefore like to take a detailed look at both of these new features in this column.

Testing with Testcontainers

At the end of my last column, I demonstrated a custom test slice for testing repositories based on PostgreSQL against a database started with Testcontainers. In reality, however, a simpler method would be used in such a case. This requires the two dependencies seen in Listing 1, including the BOM for Testcontainers.

...
<dependencies>
  ...
  <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>
  ...
</dependencies>
...
<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.testcontainers</groupId>
      <artifactId>testcontainers-bom</artifactId>
      <version>1.18.3</version>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>
...
Listing 1: Dependencies for Testcontainers

We can then use the Testcontainers JUnit 5 extension in combination with the @DynamicPropertySource provided by Spring to start a Testcontainer and declare it in the Spring ApplicationContext (see Listing 2).

@JdbcTest
@Import(GreetingTextRepository.class)
@Testcontainers
@AutoConfigureTestDatabase(replace = NONE)
class GreetingTextRepositoryTest {

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

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

    @Autowired
    GreetingTextRepository greetingTextRepository;

    @Test
    void getDefaultGreeting_shouldReturnGreetingTextFromDatabase() {
        var greetingText = greetingTextRepository.getDefaultGreetingText();

        assertThat(greetingText)
            .isEqualTo("Hallo %s.");
    }
}
Listing 2: Test with Testcontainers before Spring Boot 3.1

Since Testcontainers is now also part of the automatic dependency management in Spring Boot 3.1, we no longer have to import the BOM ourselves because this already takes place within spring-boot-dependencies and we either import this BOM indirectly via spring-boot-starter-parent or do it explicitly ourselves.

In addition to the two dependencies in Listing 1, we can now also add the dependency seen in Listing 3. This makes it possible to use the new annotation @ServiceConnection and thereby dispense with a manual registration via @DynamicPropertySource (see Listing 4).

...
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-testcontainers</artifactId>
  <scope>test</scope>
</dependency>
...
Listing 3: Spring Boot Testcontainers, dependency
@JdbcTest
@Import(GreetingTextRepository.class)
@Testcontainers
@AutoConfigureTestDatabase(replace = NONE)
class GreetingTextRepositoryConnectionDetailsTest {

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

    @Autowired
    GreetingTextRepository greetingTextRepository;

    @Test
    void getDefaultGreeting_shouldReturnGreetingTextFromDatabase() {
        var greetingText = greetingTextRepository.getDefaultGreetingText();

        assertThat(greetingText)
            .isEqualTo("Hallo %s.");
    }
}
Listing 4: Test with Testcontainers in Spring Boot 3.1

This annotation ensures that a Spring bean of type ConnectionDetails is created. With Spring Boot 3.1, beans of this type – or, more precisely, of the provided subtypes – are used for configuring the connection to external services. For databases connected via JDBC, this means that a bean of the type JdbcConnectionDetails is used. If we don’t register a bean of this type ourselves either directly or via @ServiceConnection on a Testcontainer, then it will be created with the properties specified under spring.datasource.

To identify the exact type when using @ServiceConnection on a Testcontainer, the type of the Testcontainer is generally used. The JdbcConnectionDetails mentioned above are registered for all containers that are of type JdbcDatabaseContainer. For other types of containers, such as for Redis, the name of the service is evaluated to determine which ConnectionDetails must be provided. If the name is not explicitly specified via the attribute value or name in the annotation, the name of the Docker image is analyzed.

For test slices utilizing the @AutoConfigureTestDatabase for which we want to use a connection registered with @ServiceConnection, we must manually set the attribute replace to NONE. If we don’t do this, the test slice will use an in-memory database. There may be improvements here in the future, such as via Ticket 19038, which would allow us to dispense with explicitly overwriting the defaults in this way.

Local development with Testcontainers

In addition to their use in tests, it is now also possible to utilize Testcontainers during development. To do this, we need to have within our test class path a class that can be started via the main method (see Listing 5).

public class TestApplication {

    public static void main(String[] args) {
        SpringApplication
            .from(Application::main)
            .with(ContainerConfiguration.class)
            .run(args);
    }
}
Listing 5: TestApplication class

By convention, this should be located in the same package as the application class and have the same name plus the prefix Test. Inside the main method, we take advantage of the option to load the entire configuration of the application via the from method and expand this with a test configuration using with. In this test configuration (see Listing 6), we can now register Testcontainers as beans and use the @ServiceConnection annotation to ensure that these are used as the connection.

@TestConfiguration(proxyBeanMethods = false)
public class ContainerConfiguration {

    @Bean
    @ServiceConnection
    public PostgreSQLContainer<?> postgreSQLContainer() {
        return new PostgreSQLContainer<>("postgres:15");
    }
}
Listing 6: Test configuration for Testcontainers

The application can now be launched locally by starting the TestApplication class in our IDE or by running the new Maven goal spring-boot:test-run or the Gradle task bootTestRun. If the defaults of a @ServiceConnection are insufficient for our needs or if we have to configure additional properties, it is also possible – similar to the tests – to use the DynamicPropertyRegistry within the bean registration of a test configuration (see Listing 7).

@TestConfiguration(proxyBeanMethods = false)
public class ContainerConfiguration {

    @Bean
    public PostgreSQLContainer<?> postgreSQLContainer(
            DynamicPropertyRegistry registry) {
        var container = new PostgreSQLContainer<>("postgres:15");
        registry.add("spring.datasource.url", container::getJdbcUrl);
        registry.add("spring.datasource.username", container::getUsername);
        registry.add("spring.datasource.password", container::getPassword);
        return container;
    }
}
Listing 7: Test configuration for Testcontainers with DynamicPropertyRegistry

If we use the spring-boot-devtools during development, we see that a new container is also started after reloading the application following a change. This may be desirable but also means that all previously created data will be lost after every reload. If we wish to avoid this, we can extend the bean registration of the Testcontainer with the annotation @RestartScope (see Listing 8).

@TestConfiguration(proxyBeanMethods = false)
public class ContainerConfiguration {

    @Bean
    @RestartScope
    @ServiceConnection
    public PostgreSQLContainer<?> postgreSQLContainer() {
        return new PostgreSQLContainer<>("postgres:15");
    }
}
Listing 8: Test configuration for Testcontainers with RestartScope

Alternatively, we could use the (still experimental) feature for reusable containers. However, because this deviates from the procedure documented in Spring Boot and is still experimental, I would advise using @RestartScope.

Local development with Docker Compose

In my most recent projects, it was typical to use Docker Compose to start the external services required for development. A compose.yml file existed for this purpose (see Listing 9), and docker compose up had to be run before starting the application. After the application had been stopped, docker compose down could be used to ensure that the external services were also stopped.

services:
    postgres:
        image: 'postgres:15'
        environment:
            POSTGRES_PASSWORD: password
        ports:
            - 5432
Listing 9: Example for compose.yml

Precisely this workflow is now directly supported in Spring Boot 3.1. For this, we must add the new spring-boot-docker-compose as a dependency, as shown in Listing 10. If we now start our application, we can see in the log (see Listing 11) that our compose.yml was detected and the postgres service defined there was started.

...
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-docker-compose</artifactId>
  <scope>runtime</scope>
  <optional>true</optional>
</dependency>
...
Listing 10: Spring Boot Docker Compose, dependency
... : Starting Application using Java ...
...
... : Using Docker Compose file '... /compose.yml'
... : Network javaspektrum-spring-boot31_default Creating
... : Network javaspektrum-spring-boot31_default Created
... : Container javaspektrum-spring-boot31-postgres-1 Creating
... : Container javaspektrum-spring-boot31-postgres-1 Created
... : Container javaspektrum-spring-boot31-postgres-1 Starting
... : Container javaspektrum-spring-boot31-postgres-1 Started
... : Container javaspektrum-spring-boot31-postgres-1 Waiting
... : Container javaspektrum-spring-boot31-postgres-1 Healthy
...
... : Started Application in 5.546 seconds (process running for 5.946)
...
Listing 11: Log after starting with Spring Boot Docker Compose

By default, however, stop is used for stopping rather than down. The container is then retained even after we stop the application. If we want to change this, we can do so via the configuration value spring.docker.compose.stop.command. A similar situation applies to the location and name of the Docker Compose file. These can be changed via spring.docker.compose.file.

In addition to starting and stopping the services defined within Docker Compose, Service Connections are also automatically generated here, as with the Testcontainer support. To identify which type of Service Connection is provided by a service, the name of the container image is analyzed. If this doesn’t work because a custom image is being used, there is a way to specify this yourself, as seen in Listing 12. Listing 12 also shows how to define a service that is started and stopped simultaneously with the application but for which no Service Connection should be created.

services:
    postgres:
        image: 'postgres:15'
        environment:
            POSTGRES_PASSWORD: password
        ports:
            - 5432
        labels:
            org.springframework.boot.service-connection: jdbc
    redis:
        image: 'redis:7'
        ports:
            - 6379
        labels:
            org.springframework.boot.ignore: true
Listing 12: Using labels for the configuration of Service Connections

To identify when a service has been started successfully, the defined healthcheck from the Docker Compose file is used. If nothing is defined here, Spring Boot Docker Compose waits until the defined port is reachable via TCP. This can also be switched off, and the default timeouts can be changed as well.

Conclusion

In this article, we examined the support for Testcontainers for integration tests and local development which has been added in Spring Boot 3.1 as well as the new Docker Compose functionality. We saw how these can be used to make our lives easier both in tests as well as in local development.

I am convinced that these features will be further improved and expanded by the Spring Boot team in the future so that they can be used in projects without any concerns. Of course, it is not absolutely necessary to switch over to these methods if your project already has a functioning approach, but it is definitely worth at least considering for new projects.