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 Details | Matched on |
---|---|
CassandraConnectionDetails | Containers of type CassandraContainer |
CouchbaseConnectionDetails | Containers of type CouchbaseContainer |
ElasticsearchConnectionDetails | Containers of type ElasticsearchContainer |
FlywayConnectionDetails | Containers of type JdbcDatabaseContainer |
JdbcConnectionDetails | Containers of type JdbcDatabaseContainer |
KafkaConnectionDetails | Containers of type KafkaContainer or RedpandaContainer |
LiquibaseConnectionDetails | Containers of type JdbcDatabaseContainer |
MongoConnectionDetails | Containers of type MongoDBContainer |
Neo4jConnectionDetails | Containers of type Neo4jContainer |
R2dbcConnectionDetails | Containers of type MariaDBContainer , MSSQLServerContainer , MySQLContainer , OracleContainer , or PostgreSQLContainer |
RabbitConnectionDetails | Containers of type RabbitMQContainer |
RedisConnectionDetails | Containers named “redis” |
ZipkinConnectionDetails | Containers 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
@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