Contents

Add the following to your pom.xml (Maven) or build.gradle (Gradle). Spring Boot manages compatible versions automatically when you use the Spring Boot BOM.

<!-- BOM — manages all Testcontainers module versions --> <dependencyManagement> <dependencies> <dependency> <groupId>org.testcontainers</groupId> <artifactId>testcontainers-bom</artifactId> <version>1.20.1</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.testcontainers</groupId> <artifactId>junit-jupiter</artifactId> <!-- @Testcontainers, @Container --> <scope>test</scope> </dependency> <dependency> <groupId>org.testcontainers</groupId> <artifactId>postgresql</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.testcontainers</groupId> <artifactId>kafka</artifactId> <scope>test</scope> </dependency> </dependencies>

Annotate the test class with @Testcontainers and declare the container as a static @Container field so it is shared across all test methods. Use @DynamicPropertySource to wire the container's JDBC URL into Spring's environment.

import org.junit.jupiter.api.Test; import org.testcontainers.containers.PostgreSQLContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; import static org.assertj.core.api.Assertions.assertThat; @Testcontainers class UserRepositoryTest { @Container static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine") .withDatabaseName("testdb") .withUsername("test") .withPassword("test") .withInitScript("db/schema.sql"); // runs on container start @DynamicPropertySource static void configureProperties(DynamicPropertyRegistry registry) { registry.add("spring.datasource.url", postgres::getJdbcUrl); registry.add("spring.datasource.username", postgres::getUsername); registry.add("spring.datasource.password", postgres::getPassword); } @Autowired UserRepository userRepo; @Test void savesAndFindsUser() { User user = userRepo.save(new User("Alice", "alice@example.com")); assertThat(user.id()).isNotNull(); assertThat(userRepo.findByEmail("alice@example.com")).isPresent(); } } Declare the container static so one container is started per test class rather than per test method. Starting Docker containers is slow — sharing them across methods keeps the suite fast while still using a real database.

The class below shows the implementation. Key points are highlighted in the inline comments.

import org.testcontainers.kafka.KafkaContainer; import org.testcontainers.utility.DockerImageName; @Testcontainers @SpringBootTest class OrderEventListenerTest { @Container static KafkaContainer kafka = new KafkaContainer( DockerImageName.parse("apache/kafka:3.7.0")); @DynamicPropertySource static void kafkaProperties(DynamicPropertyRegistry registry) { registry.add("spring.kafka.bootstrap-servers", kafka::getBootstrapServers); } @Autowired KafkaTemplate<String, String> kafkaTemplate; @Autowired OrderEventListener listener; @Test void consumesOrderEvent() throws Exception { kafkaTemplate.send("orders", "o1", "{\"product\":\"Widget\",\"qty\":2}"); // Wait up to 5 s for the listener to process await().atMost(5, TimeUnit.SECONDS) .untilAsserted(() -> assertThat(listener.processedCount()).isEqualTo(1)); } }

Use GenericContainer for any Docker image that doesn't have a dedicated Testcontainers module. Chain withExposedPorts, withEnv, and waitingFor to configure readiness.

import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; @Testcontainers class RedisCacheTest { @Container static GenericContainer<?> redis = new GenericContainer<>("redis:7-alpine") .withExposedPorts(6379) .waitingFor(Wait.forLogMessage(".*Ready to accept connections.*\\n", 1)); @DynamicPropertySource static void redisProperties(DynamicPropertyRegistry registry) { registry.add("spring.data.redis.host", redis::getHost); registry.add("spring.data.redis.port", () -> redis.getMappedPort(6379)); } @Test void cachesValue() { // test with real Redis } }

Build a custom image from a Dockerfile in the test resources:

import org.testcontainers.images.builder.ImageFromDockerfile; GenericContainer<?> customApp = new GenericContainer<>( new ImageFromDockerfile() .withDockerfileFromBuilder(builder -> builder .from("eclipse-temurin:21-jre-alpine") .copy("app.jar", "/app.jar") .cmd("java", "-jar", "/app.jar") .build()) .withFileFromClasspath("app.jar", "test-app.jar")) .withExposedPorts(8080) .waitingFor(Wait.forHttp("/actuator/health").forStatusCode(200));

Reusable containers survive across test runs by skipping the stop/remove on JVM exit. This dramatically speeds up local development — the container is only started once and reused on every subsequent test run until manually stopped.

// SharedContainers.java — a central class holding reusable containers public final class SharedContainers { public static final PostgreSQLContainer<?> POSTGRES; static { POSTGRES = new PostgreSQLContainer<>("postgres:16-alpine") .withReuse(true); // key flag — keeps the container alive between runs POSTGRES.start(); } private SharedContainers() {} } // Test class — uses the shared container class ProductRepositoryTest { static { // Ensures the shared container is started before Spring wires DataSource SharedContainers.POSTGRES.start(); } @DynamicPropertySource static void dbProperties(DynamicPropertyRegistry registry) { registry.add("spring.datasource.url", SharedContainers.POSTGRES::getJdbcUrl); registry.add("spring.datasource.username", SharedContainers.POSTGRES::getUsername); registry.add("spring.datasource.password", SharedContainers.POSTGRES::getPassword); } @Test void test() { /* runs against the shared container */ } } Reusable containers require testcontainers.reuse.enable=true in ~/.testcontainers.properties on the developer machine. CI environments should set this property too, or leave it unset to always start fresh containers in CI (recommended for isolation).

ComposeContainer (Testcontainers 1.19+) starts a docker-compose.yml file and exposes each service's mapped port. This lets integration tests mirror a real multi-service topology.

# src/test/resources/compose-test.yml services: postgres: image: postgres:16-alpine environment: POSTGRES_DB: testdb POSTGRES_USER: test POSTGRES_PASSWORD: test ports: - "5432" redis: image: redis:7-alpine ports: - "6379" import org.testcontainers.containers.ComposeContainer; import org.testcontainers.containers.wait.strategy.Wait; import java.io.File; @Testcontainers class FullStackTest { @Container static ComposeContainer env = new ComposeContainer( new File("src/test/resources/compose-test.yml")) .withExposedService("postgres", 5432, Wait.forListeningPort()) .withExposedService("redis", 6379, Wait.forListeningPort()); @DynamicPropertySource static void configureProperties(DynamicPropertyRegistry registry) { registry.add("spring.datasource.url", () -> "jdbc:postgresql://" + env.getServiceHost("postgres", 5432) + ":" + env.getServicePort("postgres", 5432) + "/testdb"); registry.add("spring.data.redis.host", () -> env.getServiceHost("redis", 6379)); registry.add("spring.data.redis.port", () -> env.getServicePort("redis", 6379)); } @Test void runsAgainstFullStack() { // hits both real Postgres and real Redis } }

Spring Boot 3.1+ introduces @ServiceConnection which eliminates the need for @DynamicPropertySource — Spring auto-wires the container's connection properties.

import org.springframework.boot.testcontainers.service.connection.ServiceConnection; @SpringBootTest @Testcontainers class OrderServiceIntegrationTest { @Container @ServiceConnection // no @DynamicPropertySource needed static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine"); @Container @ServiceConnection static KafkaContainer kafka = new KafkaContainer(DockerImageName.parse("apache/kafka:3.7.0")); @Autowired OrderService orderService; @Test void placesOrderAndPublishesEvent() { Order order = orderService.place("Widget", 3); assertThat(order.id()).isNotNull(); // Kafka event verified via consumer... } }

For projects not yet on Spring Boot 3.1, use the @DynamicPropertySource approach shown in the PostgreSQL section. Both work identically — @ServiceConnection is just syntactic sugar on top.