Since Spring Boot 3.1, @ServiceConnection can be used to establish a connection to a remote service in such a way that specified connection details take precedence over any connection configured using configuration properties. The @ServiceConnection is super useful in development and test environments where we can use Testcontainers to mock external services such as databases or message queues.
1. What is a Service Connection?
A service connection is a connection to any remote service. For example, it can be a connection to the backend database, message queues, elastic search instance etc. Generally, the connection details to remote services are defined in application.properties file or @Configuration annotated fields. When the application starts, these connection details are used to create connections when accessing the remote service.
In development or test environments, we may want to connect to a Testcontainer to avoid setting up the additional softwares on the local machine. Rather than setting up additional properties, we can use @ServiceConnection to override the default connection details and use the annotated Testcontainer as the backend service.
In the following example, we are creating a service connection to PostgreSQL database with default connection details. Internally, it creates an R2dbcConnectionDetails bean. When the tests are run, the code will enquire this bean for connection URL, username, password and other such details.
@SpringBootTest
public class TestAppIntegration {
@Container
@ServiceConnection
PostgreSQLContainer postgres = new PostgreSQLContainer(DockerImageName.parse("postgres:15.1"));
// tests...
}
The above code can be written in a more verbose manner, without @ServiceConnection, as follows. The @ServiceConnection removes the need to write @DynamicPropertySource property overrides.
@SpringBootTest
public class TestAppIntegration {
@Container
PostgreSQLContainer postgres = new PostgreSQLContainer(DockerImageName.parse("postgres:15.1"));
@DynamicPropertySource
static void datasourceProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
// tests...
}
2. Maven
To utilize the @ServiceConnection, we need to include the spring-boot-testcontainers dependency. Additionally, we need to include other Testcontainer dependencies support for JUnit and the required container.
<!-- Includes @ServiceConnection support -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
<!-- Includes JUnit support for Testcontainers using @Container annotation-->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>1.18.1</version>
<scope>test</scope>
</dependency>
<!-- Includes PostgreSQLContainer support. Replace it with the container used in your application -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>1.17.6</version>
<scope>test</scope>
</dependency>
3. Using @ServiceConnection in Integration Tests
The service connections can be used in integration testing in two ways, either annotate the container field or annotate the container bean provided by test configuration.
3.1. Using @ServiceConnection on Class Field
This is the simplest approach. Simply annotate the @Container annotated field with @ServiceConnection and Spring boot takes care of the rest.
@SpringBootTest
public class ServiceConnectionFieldTest {
@Autowired
ItemRepository itemRepository;
@Container
@ServiceConnection
static PostgreSQLContainer postgres = new PostgreSQLContainer(DockerImageName.parse("postgres:15.1"))
.withUsername("testUser")
.withPassword("testSecret")
.withDatabaseName("testDatabase");
@BeforeAll
static void setup(){
postgres.start();
}
@Test
void testConnection(){
Assertions.assertTrue(postgres.isRunning());
Assertions.assertEquals("testUser", postgres.getUsername());
Assertions.assertEquals("testSecret", postgres.getPassword());
Assertions.assertEquals("testDatabase", postgres.getDatabaseName());
}
@Test
void testSaveItem(){
Item item = new Item(null, "Grocery");
Item savedItem = itemRepository.save(item);
Assertions.assertNotNull(savedItem.getId());
}
}
3.2. Using @ServiceConnection on Test Configuration
We can use the @ServiceConnection on a bean configured in @TestConfiguration class. Such configuration gives the additional benefit of using the @RestartScope annotation which prevents the container restarts during automatic reloads by Spring Boot DevTools.
@TestConfiguration(proxyBeanMethods = false)
public static class TestContainersConfiguration {
@Bean
@ServiceConnection
@RestartScope
public PostgreSQLContainer<?> postgreSQLContainer() {
return new PostgreSQLContainer(DockerImageName.parse("postgres:15.1"))
.withUsername("testUser")
.withPassword("testSecret")
.withDatabaseName("testDatabase");
}
}
Next, we can import the configuration in the test class as follows:
@SpringBootTest
@Import(TestContainersConfiguration.class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class ServiceConnectionBeanTest {
@Autowired
ItemRepository itemRepository;
@Autowired
PostgreSQLContainer postgres;
@BeforeAll
void setup() {
postgres.start();
}
//tests
}
4. Using @ServiceConnection in Development Environment
To use the @Testcontainer at development time, we need to launch the application from the “test” classpath so that the application can use all the test dependencies, where the containers are generally defined. Then we need to launch the “main” application from this class and import the test configuration where testcontainers are defined.
By default, Spring Boot manages the lifecycle of Container beans and containers are started and stopped automatically.
In the following example, when the application runs, it utilizes the connection details provided by @ServiceConnection rather than using the application.properties.
public class AppTest {
public static void main(String[] args) {
SpringApplication.from(App::main)
.with(TestContainersConfiguration.class)
.run(args);
}
@TestConfiguration(proxyBeanMethods = false)
public static class TestContainersConfiguration {
@Bean
@ServiceConnection
@RestartScope
public PostgreSQLContainer<?> postgreSQLContainer() {
return new PostgreSQLContainer(DockerImageName.parse("postgres:15.1"))
.withUsername("testUser")
.withPassword("testSecret")
.withDatabaseName("testDatabase");
}
}
}
5. Conclusion
The @ServiceConnection annotation is a useful addition for Spring boot applications that use Testcontainers for development and testing purposes. It provides a clean and non-verbose way to override the connection details, opposite to @DynamicPropertySource which requires explicit overrides.
Happy Learning !!
Comments