Using Testcontainers for Local Development and Integration Testing In Spring Boot 3.1

In this blog post we will see how we can use docker containers during development time using Testcontainers in Spring Boot 3.1 which simplifies the local development efforts by providing infrastructure.

In one of my previous blog posts, I have already shown how can we start docker containers using Testcontainers during local development.

I have also covered extensively in my previous blog posts, how we can use Testcontainers for integration testing.( PostgreSQL , DB2, Oracle, MySQL, RabbitMQ, Kafka)

Service Connections

Spring Boot 3.1 introduced some new features like service connection which simplifies the use of Testcontainers for integration testing and local development.

A service connection is a connection to any remote service such as database, Message Queue, Elastic Search or Cache Service.

Such connections are represented in an application by ConnectionDetails beans. These beans provide the necessary details to establish a connection to a remove service and Spring Boot’s auto-configuration has been updated to consume ConnectionDetails beans. 

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE})
public @interface ServiceConnection {
    @AliasFor("name")
    String value() default "";

    @AliasFor("value")
    String name() default "";

    Class<? extends ConnectionDetails>[] type() default {};
}Code language: Java (java)

When service connection beans are available, they will take precedence over any connection-related configuration properties. Configuration properties that are not related to the connection itself, such as properties that control the size and behaviour of a connection pool, will still be used.

TestContainers and Integration Tests

When using Testcontainers for integration testing, we use  @DynamicPropertySource annotation to get container settings and register them with spring configuration to use them.

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@Testcontainers
public class BaseIT {
    @Autowired
    protected TestRestTemplate testRestTemplate ;

    @Container   
    public static PostgreSQLContainer<?> postgresDB = new PostgreSQLContainer<>("postgres:13.2");

    @DynamicPropertySource
    public static void properties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url",postgresDB::getJdbcUrl);
        registry.add("spring.datasource.username", postgresDB::getUsername);
        registry.add("spring.datasource.password", postgresDB::getPassword);

    }
}
Code language: Java (java)

The above code can be simplified in following way in spring Boot 3.1

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@Testcontainers
public class BaseIT {
    @Autowired
    protected TestRestTemplate testRestTemplate ;

    @Container
    @ServiceConnection(type = JdbcConnectionDetails.class)
    public static PostgreSQLContainer<?> postgresDB = new PostgreSQLContainer<>("postgres:13.2");

}
Code language: Java (java)

Due to @ServiceConnection, the above configuration allows JDBCConnectionDetails beans in the application to communicate with PostgreSQL running inside the Testcontainers-managed Docker container. This is done by automatically defining a JdbcConnectionDetails bean which is then used by the JPA auto-configuration, overriding any connection-related configuration properties.

Service connection annotations are processed by ContainerConnectionDetailsFactory classes registered with spring.factories. A ContainerConnectionDetailsFactory can create a ConnectionDetails bean based on a specific Container subclass, or the Docker image name.

The following service connection factories are provided in the spring-boot-testcontainers jar:

Connection DetailsMatched on
CassandraConnectionDetailsContainers of type CassandraContainer
CouchbaseConnectionDetailsContainers of type CouchbaseContainer
ElasticsearchConnectionDetailsContainers of type ElasticsearchContainer
FlywayConnectionDetailsContainers of type JdbcDatabaseContainer
JdbcConnectionDetailsContainers of type JdbcDatabaseContainer
KafkaConnectionDetailsContainers of type KafkaContainer or RedpandaContainer
LiquibaseConnectionDetailsContainers of type JdbcDatabaseContainer
MongoConnectionDetailsContainers of type MongoDBContainer
Neo4jConnectionDetailsContainers of type Neo4jContainer
R2dbcConnectionDetailsContainers of type MariaDBContainerMSSQLServerContainerMySQLContainerOracleContainer, or PostgreSQLContainer
RabbitConnectionDetailsContainers of type RabbitMQContainer
RedisConnectionDetailsContainers named “redis”
ZipkinConnectionDetailsContainers named “openzipkin/zipkin”

By default all applicable connection details beans will be created for a given Container. For example, a PostgreSQLContainer will create both JdbcConnectionDetails and R2dbcConnectionDetails.

If you want to create only a subset of the applicable types, you can use the type attribute of @ServiceConnection.

@ServiceConnection(type = JdbcConnectionDetails.class)Code language: Java (java)
@ServiceConnection(type = R2dbcConnectionDetails.class)
Code language: Java (java)

By default Container.getDockerImageName() is used to obtain the name used to find connection details. If you are using a custom docker image, you can use the name attribute of @ServiceConnection to override it.

For example, if you have a GenericContainer using a Docker image of mydatabase/mypostgres, you’d use @ServiceConnection(name="redis") to ensure JdbcConnectionDetails are created.

Notice

You can continue to use @DynamicPropertySource annotation to add dynamic property values to the Spring Environment for technologies that don’t yet have @ServiceConnection support.

Using Testcontainers at Development Time

It is possible to utilise Testcontainers both during development and for integration testing. This method eliminates the need to manually setup things like database servers by enabling developers to instantly start containers for the services that the application depends on. With the exception of the fact that your container setup is in Java rather than YAML, using Testcontainers in this manner offers capabilities comparable to that of Docker Compose.

To use Testcontainers at development time, you need to launch your application using your “test” classpath rather than “main”. This will allow you to access all declared test dependencies and give you a natural place to write your test configuration.

To launch your application using docker containers managed by the Testcontainers, create a class in src/test directory.

For example, if your main application is in src/main/java/com/example/MyApplication.java, you should create src/test/java/com/example/ApplicationLocalDevelopment.java

The ApplicationLocalDevelopment class can use the SpringApplication.from(…​) method to launch the real application

public class ApplicationLocalDevelopment {

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

}Code language: Java (java)

The Container instances that you want to launch with your application must also be defined. You must make sure that the spring-boot-testcontainers module has been included as a test dependency in order to accomplish this. Once that has been done, you may make a @TestConfiguration class that declares @Bean methods for the containers you wish to launch.

You can also annotate your @Bean methods with @ServiceConnection in order to create ConnectionDetails beans to automatically extract the required properties and establish connections to the containers

@TestConfiguration(proxyBeanMethods = false)
class DevContainersConfiguration{
    @Bean
    @ServiceConnection
    PostgreSQLContainer<?> postgresDBContainer() {
        return new PostgreSQLContainer<>("postgres:13.2");
    }
}Code language: Java (java)

Using Dynamic Properties at Development Time

If you want to contribute dynamic properties at development time from your Container @Bean methods, you can do so by injecting a DynamicPropertyRegistry. This works in a similar way to the @DynamicPropertySource annotation that you can use in your tests. It allows you to add properties that will become available once your container has started.

@TestConfiguration(proxyBeanMethods = false)
public class DevContainersConfiguration{

    @Bean
    public MongoDBContainer monogDbContainer(DynamicPropertyRegistry properties) {
        MongoDBContainer container = new MongoDBContainer("mongo:5.0");
        properties.add("spring.data.mongodb.host", container::getHost);
        properties.add("spring.data.mongodb.port", container::getFirstMappedPort);
        return container;
    }

}Code language: Java (java)

Sharing Container Declarations between Tests and Development

Declaring Container instances as static fields is a typical design strategy when utilizing Testcontainers. These fields are frequently defined right on the test class. They can also be declared on a parent class or on an interface that the test implements.

For example below BaseIT class declares Postgres container. This base class is extended by all the test classes so that they do not need to repeat the code

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@Testcontainers
public class BaseIT {
    @Autowired
    protected TestRestTemplate testRestTemplate ;

    @Container
    @ServiceConnection(type = JdbcConnectionDetails.class)
    public static PostgreSQLContainer<?> postgresDB = new PostgreSQLContainer<>("postgres:13.2");

}Code language: Java (java)

If Parent class or Interface to defines container like above, you can import these declaration classes using @ImportTestcontainers annotation  rather than defining you containers as @Bean in test configuration classes.

Using @ImportTestcontainers annotation you can share container definitions between Development Testing environment.

The ApplicationLocalDevelopment class can be written below way, If you are making use of @ImportTestcontainers annotation.

public class ApplicationLocalDevelopment2 {

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

	@TestConfiguration(proxyBeanMethods = false)
	@ImportTestcontainers(BaseIT.class)
	static class LocalDevConfig {

	}

}Code language: Java (java)

You can download the source code for this blog post from GitHub

Similar Posts