Spring Boot @ServiceConnection

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 !!

Sourcecode on Github

Comments

Subscribe
Notify of
guest
1 Comment
Most Voted
Newest Oldest
Inline Feedbacks
View all comments

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.