Contents
- Dependency
- PostgreSQL Container
- Kafka Container
- Generic & Custom Docker Images
- Reusable Containers
- Docker Compose
- Spring Boot Integration
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.