@ServiceConnection

Marks a Testcontainers container so Spring Boot connects the application to it automatically, with no URLs, ports or passwords in test properties.

Signature
@ServiceConnection(name = "redis")
  • Added in Spring Boot Spring Boot 3.1

Parameters

ParameterTypeDescription
$name / valueoptionalString= ""The service name used to find a connection details factory. Empty means derive it from the container's image name.
$typeoptionalClass<? extends ConnectionDetails>[]= {}Restricts which ConnectionDetails types are created. Empty means all that apply.

Return value

n/aCreates ConnectionDetails beans from the annotated container so auto-configured clients connect to it instead of the configured properties.

Errors

Startup fails if no connection details factory supports the container, for example a GenericContainer with an unrecognised image and no name.

A container started by Testcontainers listens on a random host port with a generated password. Before @ServiceConnection, every test had to copy those values into spring.datasource.url and friends with @DynamicPropertySource. Now you annotate the container, and Spring Boot creates a connection details bean (such as JdbcConnectionDetails) from it. Auto-configuration prefers connection details over properties, so the DataSource, Flyway and JPA all point at the container.

It needs spring-boot-testcontainers on the test classpath. In Spring Boot 4 that comes with Testcontainers 2, where module classes live in their own packages: org.testcontainers.postgresql.PostgreSQLContainer, which is no longer a generic type.

Two places to put it

On a static container field in a test class with @Testcontainers and @Container. JUnit starts the container before the class and stops it afterwards. Simple, and fine for one class.

On a @Bean method in a @TestConfiguration that tests @Import. The container is then a bean: Spring starts it before anything that needs it and stops it when the context closes. This is the better choice for a suite, because of context caching. When a field-managed container, for example one declared in a shared base class, stops at the end of class A, a cached context reused by class B still points at the dead port, and B fails with connection refused. A container bean lives exactly as long as the context that uses it.

How the service is recognised

For typed containers such as PostgreSQLContainer, the container class tells Boot which connection details to create. For a GenericContainer, Boot uses the image name, so redis:7-alpine is recognised as Redis. When you pull through a private mirror with a different image name, set @ServiceConnection(name = "redis"). The type attribute restricts which connection details are created when a container could serve more than one.

Common mistakes

  • A non-static container field: a fresh database for every test method, and much slower runs.
  • Leaving a test spring.datasource.url in place and expecting it to win. Connection details take precedence.
  • Expecting Hibernate to create the tables. It only does that by default for embedded databases; against a container the schema comes from your Flyway or Liquibase migrations, or from spring.jpa.hibernate.ddl-auto set for the test.
  • Using latest image tags. Pin the same major version you run in production, so a new upstream release cannot change test results without a deliberate upgrade.

The same configuration can start your application locally: a main method in src/test/java that calls SpringApplication.from(...).with(...) runs the real app against fresh containers, with nothing to install.

Example

Example · java
import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.data.jpa.repository.JpaRepository;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.postgresql.PostgreSQLContainer;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;

@Entity
class Booking {

    @Id
    @GeneratedValue
    Long id;

    String guest;

    protected Booking() {}

    Booking(String guest) {
        this.guest = guest;
    }
}

interface BookingRepository extends JpaRepository<Booking, Long> {}

// PostgreSQL is not an embedded database, so Hibernate does not create tables
// by default. Flyway migrations would; this example has none, so create-drop.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE,
        properties = "spring.jpa.hibernate.ddl-auto=create-drop")
@Testcontainers
class BookingPersistenceTest {

    // Started once for the class; Boot reads its URL, user and password.
    // No spring.datasource.* properties and no @DynamicPropertySource needed.
    @Container
    @ServiceConnection
    static PostgreSQLContainer postgres = new PostgreSQLContainer("postgres:17-alpine");

    @Autowired
    BookingRepository bookings;

    @Test
    void savesToRealPostgres() {
        bookings.save(new Booking("Ada Lovelace"));
        assertThat(bookings.count()).isEqualTo(1);
    }
}

When to use it

  • Run repository and integration tests against the same PostgreSQL major version as production.
  • Share one PostgreSQL and one Redis container across every integration test class in a suite.
  • Give new team members a one-click local run of the API with no database to install.

Frequently asked questions

Do I still need @DynamicPropertySource with Testcontainers?

Not for services Boot supports with connection details, such as JDBC databases, Redis, RabbitMQ and Kafka. Keep @DynamicPropertySource for your own app.* properties, for example the URL of a container running a mock HTTP server.

Why do my tests fail with connection refused after the first test class?

A static @Container field was stopped when its class finished, but Spring reused the cached context that still points at it. Move the container to a @Bean method in a @TestConfiguration so its lifecycle follows the context.

Which PostgreSQLContainer class should I import in Spring Boot 4?

org.testcontainers.postgresql.PostgreSQLContainer from the testcontainers-postgresql module. It is not generic, so declare it as PostgreSQLContainer rather than PostgreSQLContainer<?>.

More examples

Containers as beans in a shared test configuration

Any test that adds <code>@Import(ContainersConfig.class)</code> gets PostgreSQL and Redis wired in. Because the containers are beans of the cached context, every test class that shares the configuration also shares the running containers, and they are stopped only when the context is closed. <code>proxyBeanMethods = false</code> avoids a CGLIB proxy that these methods do not need.

Example · java
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.postgresql.PostgreSQLContainer;

// Containers as beans: Spring starts them before the beans that need them
// and stops them when the cached context is closed.
@TestConfiguration(proxyBeanMethods = false)
public class ContainersConfig {

    @Bean
    @ServiceConnection
    PostgreSQLContainer postgres() {
        return new PostgreSQLContainer("postgres:17-alpine");
    }

    // A GenericContainer is matched by image name; naming the service keeps it
    // working when the image comes from a mirror with a different name.
    @Bean
    @ServiceConnection(name = "redis")
    GenericContainer<?> redis() {
        return new GenericContainer<>("redis:7-alpine").withExposedPorts(6379);
    }
}

Running the application locally against containers

<code>SpringApplication.from</code> takes the real application's <code>main</code> method and <code>with</code> adds the test configuration, so the same container beans now back a local run. Start <code>TestShopApplication</code> from the IDE and the app comes up with an empty PostgreSQL, migrated by Flyway if you use it. Nobody needs a local database installed.

Example · java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
import org.testcontainers.postgresql.PostgreSQLContainer;

// Normally in src/main/java.
@SpringBootApplication
class ShopApplication {
    public static void main(String[] args) {
        SpringApplication.run(ShopApplication.class, args);
    }
}

@TestConfiguration(proxyBeanMethods = false)
class DevContainers {

    @Bean
    @ServiceConnection
    PostgreSQLContainer postgres() {
        return new PostgreSQLContainer("postgres:17-alpine");
    }
}

// In src/test/java: run this main method to start the app against a container.
public class TestShopApplication {
    public static void main(String[] args) {
        SpringApplication.from(ShopApplication::main)
                .with(DevContainers.class)
                .run(args);
    }
}

The test dependencies

<code>spring-boot-testcontainers</code> provides <code>@ServiceConnection</code> and the connection details factories. The Testcontainers modules use the 2.x artifact names with a <code>testcontainers-</code> prefix, and their versions are managed by Spring Boot's dependency management. A running Docker-compatible engine is needed on the machine and in CI.

Example · xml
<!-- Versions come from the Spring Boot parent. -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-testcontainers</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>testcontainers-postgresql</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>testcontainers-junit-jupiter</artifactId>
    <scope>test</scope>
</dependency>

Discussion

  • Be the first to comment on this lesson.