Testcontainers Spring Boot: schmerzfreie Integrationstests
Vollständiger Leitfaden zur Konfiguration von Testcontainers mit Spring Boot 3.4. PostgreSQL, Redis und Kafka in Docker-Containern für zuverlässige, reproduzierbare Integrationstests.

Integrationstests stellen eine erhebliche Herausforderung in der Spring-Boot-Entwicklung dar. Tests gegen eine echte PostgreSQL-Datenbank oder einen Kafka-Broker erfordern den Betrieb einer aufwendigen Infrastruktur. Testcontainers löst dieses Problem, indem Docker-Container während der Tests bei Bedarf gestartet werden und so isolierte und reproduzierbare Umgebungen garantieren.
Spring Boot 3.4 enthält native Testcontainers-Unterstützung mit Auto-Konfiguration. Die Abhängigkeiten spring-boot-testcontainers vereinfachen die Einrichtung drastisch und ermöglichen die Wiederverwendung von Containern zwischen Tests.
Die Integration zwischen Testcontainers und Spring Boot verstehen
Testcontainers stellt eine Java-API bereit, um Docker-Container während der Testausführung zu starten. Anstatt externe Abhängigkeiten zu mocken oder gemeinsam genutzte Test-Datenbanken zu pflegen, erhält jeder Testlauf seine eigene isolierte Instanz.
Die Architektur stützt sich auf drei Hauptkomponenten: die Testcontainers-Bibliothek, die Docker steuert, spezialisierte Module für jede Technologie (PostgreSQL, Redis, Kafka) und die Spring-Boot-Integration, die die Verbindungsparameter automatisch injiziert.
<!-- pom.xml -->
<dependencies>
<!-- Main Testcontainers dependency -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
<!-- PostgreSQL module -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
<!-- JUnit 5 support -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<!-- Testcontainers BOM for version management -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-bom</artifactId>
<version>1.20.4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>Das Testcontainers-BOM stellt die Versionskonsistenz zwischen allen im Projekt verwendeten Modulen sicher.
Grundkonfiguration mit PostgreSQL
Der häufigste Anwendungsfall sind Tests gegen eine echte PostgreSQL-Datenbank. Spring Boot 3.4 bietet zwei Ansätze: die Annotation @ServiceConnection für die Auto-Konfiguration oder die manuelle Konfiguration über @DynamicPropertySource.
@DataJpaTest
@Testcontainers
@AutoConfigureTestDatabase(replace = Replace.NONE)
class UserRepositoryIntegrationTest {
// Starts a PostgreSQL container before tests
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>(
DockerImageName.parse("postgres:16-alpine")
);
@Autowired
private UserRepository userRepository;
@Test
void shouldSaveAndRetrieveUser() {
// Given: a user to persist
User user = new User();
user.setEmail("test@example.com");
user.setName("Test User");
// When: save and retrieve
User saved = userRepository.save(user);
Optional<User> found = userRepository.findById(saved.getId());
// Then: user is correctly persisted
assertThat(found).isPresent();
assertThat(found.get().getEmail()).isEqualTo("test@example.com");
}
@Test
void shouldFindUserByEmail() {
// Given: a user in database
User user = new User();
user.setEmail("search@example.com");
user.setName("Search User");
userRepository.save(user);
// When: search by email
Optional<User> found = userRepository.findByEmail("search@example.com");
// Then: user is found
assertThat(found).isPresent();
assertThat(found.get().getName()).isEqualTo("Search User");
}
}Die Annotation @ServiceConnection erkennt automatisch den Containertyp und konfiguriert die entsprechenden Spring-Properties (spring.datasource.url, spring.datasource.username usw.). Dieser Ansatz beseitigt repetitiven Konfigurationscode.
Mit @Container an einem statischen Feld startet der Container einmalig vor allen Tests der Klasse und stoppt nach dem letzten Test. Für einen Container pro Test ist ein nicht-statisches Instanzfeld zu verwenden.
Manuelle Konfiguration mit @DynamicPropertySource
Manche Szenarien erfordern eine feinere Kontrolle über die injizierten Properties. Die Annotation @DynamicPropertySource erlaubt es, Konfigurationswerte explizit zu definieren.
@DataJpaTest
@Testcontainers
@AutoConfigureTestDatabase(replace = Replace.NONE)
class OrderRepositoryIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>(
DockerImageName.parse("postgres:16-alpine")
)
// Specific container configuration
.withDatabaseName("orders_test")
.withUsername("test_user")
.withPassword("test_password")
// SQL initialization script
.withInitScript("db/init-orders.sql");
// Manual injection of dynamic properties
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
// JDBC URL generated dynamically with mapped port
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
// Additional properties if needed
registry.add("spring.jpa.hibernate.ddl-auto", () -> "validate");
}
@Autowired
private OrderRepository orderRepository;
@Autowired
private EntityManager entityManager;
@Test
void shouldPersistOrderWithItems() {
// Given: an order with items
Order order = new Order();
order.setOrderNumber("ORD-2026-001");
order.setStatus(OrderStatus.PENDING);
OrderItem item = new OrderItem();
item.setProductId(1L);
item.setQuantity(2);
item.setUnitPrice(BigDecimal.valueOf(29.99));
order.addItem(item);
// When: save the order
Order saved = orderRepository.save(order);
entityManager.flush();
entityManager.clear();
// Then: order and its items are persisted
Order found = orderRepository.findById(saved.getId()).orElseThrow();
assertThat(found.getItems()).hasSize(1);
assertThat(found.getItems().get(0).getQuantity()).isEqualTo(2);
}
}Das Initialisierungsskript withInitScript bereitet das Schema vor oder fügt Referenzdaten ein, bevor die Tests ausgeführt werden.
Vollständige Spring-Boot-Integrationstests
Um die gesamte Anwendung mit allen geladenen Komponenten zu testen, ersetzt @SpringBootTest die Annotation @DataJpaTest. Diese Konfiguration startet den vollständigen Spring-Kontext zusammen mit dem PostgreSQL-Container.
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@Testcontainers
class UserServiceIntegrationTest {
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>(
DockerImageName.parse("postgres:16-alpine")
);
@Autowired
private UserService userService;
@Autowired
private TestRestTemplate restTemplate;
@Test
void shouldCreateUserViaApi() {
// Given: a creation request
CreateUserRequest request = new CreateUserRequest(
"api@example.com",
"API User",
"securePassword123"
);
// When: call the REST API
ResponseEntity<UserResponse> response = restTemplate.postForEntity(
"/api/users",
request,
UserResponse.class
);
// Then: user is created successfully
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
assertThat(response.getBody()).isNotNull();
assertThat(response.getBody().email()).isEqualTo("api@example.com");
}
@Test
void shouldRetrieveUserById() {
// Given: an existing user
UserResponse created = userService.createUser(
new CreateUserRequest("retrieve@example.com", "Retrieve User", "password")
);
// When: retrieve by ID
ResponseEntity<UserResponse> response = restTemplate.getForEntity(
"/api/users/" + created.id(),
UserResponse.class
);
// Then: user is returned
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody().name()).isEqualTo("Retrieve User");
}
}Das automatisch konfigurierte TestRestTemplate zeigt auf den Server, der auf einem zufälligen Port gestartet wurde, und vermeidet so Portkonflikte zwischen parallelen Tests.
Bereit für deine Spring Boot-Interviews?
Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.
Wiederverwendung von Containern zwischen Tests
Der Start eines Docker-Containers dauert mehrere Sekunden. Um die Testausführung zu beschleunigen, ermöglicht Spring Boot 3.4 die Wiederverwendung von Containern über eine zentralisierte Konfiguration.
@TestConfiguration(proxyBeanMethods = false)
public class TestcontainersConfiguration {
// Reusable container bean across all tests
@Bean
@ServiceConnection
PostgreSQLContainer<?> postgresContainer() {
return new PostgreSQLContainer<>(DockerImageName.parse("postgres:16-alpine"))
.withReuse(true)
.withLabel("reuse.UUID", "e06d7a87-7d7d-472e-a047-7c2c6d4b5f7a");
}
@Bean
@ServiceConnection
RedisContainer redisContainer() {
return new RedisContainer(DockerImageName.parse("redis:7-alpine"))
.withReuse(true)
.withLabel("reuse.UUID", "b3c8f9d2-4a5e-4c8d-9f2a-1b3c5d7e9f0a");
}
}Die Tests importieren diese Konfiguration, um dieselben Container gemeinsam zu nutzen.
@SpringBootTest
@Import(TestcontainersConfiguration.class)
class ProductServiceIntegrationTest {
@Autowired
private ProductService productService;
@Autowired
private ProductRepository productRepository;
@Test
void shouldCacheProductDetails() {
// Given: a product in database
Product product = new Product();
product.setName("Cached Product");
product.setPrice(BigDecimal.valueOf(99.99));
productRepository.save(product);
// When: two successive calls
ProductDto first = productService.getProductById(product.getId());
ProductDto second = productService.getProductById(product.getId());
// Then: second call uses cache
assertThat(first).isEqualTo(second);
}
}Um die Container-Wiederverwendung zu aktivieren, muss die Konfiguration in ~/.testcontainers.properties ergänzt werden:
# ~/.testcontainers.properties
testcontainers.reuse.enable=trueBei der Wiederverwendung von Containern bleiben die Daten zwischen Ausführungen erhalten. Es empfiehlt sich, @Sql oder @BeforeEach zu verwenden, um Tabellen vor jedem Test zu leeren, oder pro Testklasse ein anderes Schema zu konfigurieren.
Tests mit Redis und verteiltem Cache
Testcontainers unterstützt Redis zum Testen von Cache-Funktionalitäten. Das Redis-Modul stellt einen vorkonfigurierten, einsatzbereiten Container bereit.
@SpringBootTest
@Testcontainers
class CacheServiceIntegrationTest {
@Container
@ServiceConnection
static RedisContainer redis = new RedisContainer(
DockerImageName.parse("redis:7-alpine")
);
@Autowired
private CacheService cacheService;
@Autowired
private StringRedisTemplate redisTemplate;
@Test
void shouldStoreAndRetrieveFromCache() {
// Given: a value to cache
String key = "user:123";
String value = "{\"id\":123,\"name\":\"Cached User\"}";
// When: store in cache
cacheService.put(key, value, Duration.ofMinutes(10));
// Then: value is retrievable
String cached = cacheService.get(key);
assertThat(cached).isEqualTo(value);
}
@Test
void shouldExpireAfterTtl() throws InterruptedException {
// Given: a value with short TTL
String key = "expiring:key";
cacheService.put(key, "temporary", Duration.ofSeconds(1));
// When: wait for expiration
Thread.sleep(1500);
// Then: key has expired
String cached = cacheService.get(key);
assertThat(cached).isNull();
}
@Test
void shouldIncrementCounter() {
// Given: a counter key
String counterKey = "page:views:homepage";
// When: multiple increments
Long first = redisTemplate.opsForValue().increment(counterKey);
Long second = redisTemplate.opsForValue().increment(counterKey);
Long third = redisTemplate.opsForValue().increment(counterKey);
// Then: counter increments correctly
assertThat(first).isEqualTo(1);
assertThat(second).isEqualTo(2);
assertThat(third).isEqualTo(3);
}
}Spring Boot erkennt den RedisContainer automatisch über @ServiceConnection und konfiguriert spring.data.redis.host und spring.data.redis.port.
Tests mit Kafka und asynchronem Messaging
Ereignisorientierte Anwendungen erfordern Tests gegen einen echten Kafka-Broker. Testcontainers bietet ein Kafka-Modul, das einen Single-Node-Cluster startet, der für Tests geeignet ist.
@SpringBootTest
@Testcontainers
@EmbeddedKafka(partitions = 1, topics = {"order-events"})
class OrderEventIntegrationTest {
@Container
@ServiceConnection
static KafkaContainer kafka = new KafkaContainer(
DockerImageName.parse("confluentinc/cp-kafka:7.6.0")
);
@Autowired
private KafkaTemplate<String, OrderEvent> kafkaTemplate;
@Autowired
private OrderEventConsumer orderEventConsumer;
@Test
void shouldPublishAndConsumeOrderEvent() throws Exception {
// Given: an order event
OrderEvent event = new OrderEvent(
"ORD-2026-100",
OrderEventType.CREATED,
LocalDateTime.now()
);
// When: publish to Kafka
kafkaTemplate.send("order-events", event.orderId(), event).get();
// Then: event is consumed (with timeout)
await()
.atMost(Duration.ofSeconds(10))
.untilAsserted(() -> {
assertThat(orderEventConsumer.getReceivedEvents())
.hasSize(1)
.first()
.extracting(OrderEvent::orderId)
.isEqualTo("ORD-2026-100");
});
}
}@Component
public class OrderEventConsumer {
private final List<OrderEvent> receivedEvents = new CopyOnWriteArrayList<>();
@KafkaListener(topics = "order-events", groupId = "test-group")
public void consume(OrderEvent event) {
receivedEvents.add(event);
}
public List<OrderEvent> getReceivedEvents() {
return List.copyOf(receivedEvents);
}
public void clear() {
receivedEvents.clear();
}
}Die Awaitility-Bibliothek übernimmt asynchrone Assertions mit Timeout und vermeidet so anfällige Thread.sleep-Aufrufe in Tests.
Multi-Container-Konfiguration mit Docker Compose
Für komplexe Anwendungen mit mehreren voneinander abhängigen Diensten unterstützt Testcontainers Docker-Compose-Dateien.
# src/test/resources/docker-compose-test.yml
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: testdb
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpass
ports:
- "5432"
redis:
image: redis:7-alpine
ports:
- "6379"
localstack:
image: localstack/localstack:3.0
environment:
SERVICES: s3,sqs
DEFAULT_REGION: eu-west-1
ports:
- "4566"@SpringBootTest
@Testcontainers
class FullStackIntegrationTest {
@Container
static DockerComposeContainer<?> environment = new DockerComposeContainer<>(
new File("src/test/resources/docker-compose-test.yml")
)
.withExposedService("postgres", 5432)
.withExposedService("redis", 6379)
.withExposedService("localstack", 4566)
.waitingFor("postgres", Wait.forListeningPort())
.waitingFor("redis", Wait.forListeningPort())
.waitingFor("localstack", Wait.forLogMessage(".*Ready\\.$", 1));
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
// PostgreSQL configuration
String postgresHost = environment.getServiceHost("postgres", 5432);
Integer postgresPort = environment.getServicePort("postgres", 5432);
registry.add("spring.datasource.url",
() -> "jdbc:postgresql://" + postgresHost + ":" + postgresPort + "/testdb");
registry.add("spring.datasource.username", () -> "testuser");
registry.add("spring.datasource.password", () -> "testpass");
// Redis configuration
String redisHost = environment.getServiceHost("redis", 6379);
Integer redisPort = environment.getServicePort("redis", 6379);
registry.add("spring.data.redis.host", () -> redisHost);
registry.add("spring.data.redis.port", () -> redisPort);
// LocalStack S3 configuration
String localstackHost = environment.getServiceHost("localstack", 4566);
Integer localstackPort = environment.getServicePort("localstack", 4566);
registry.add("aws.s3.endpoint",
() -> "http://" + localstackHost + ":" + localstackPort);
}
@Autowired
private FileStorageService fileStorageService;
@Test
void shouldUploadFileToS3() {
// Given: a file to upload
byte[] content = "Test file content".getBytes();
String fileName = "test-file.txt";
// When: upload to S3 via LocalStack
String url = fileStorageService.upload(fileName, content);
// Then: file is accessible
assertThat(url).contains(fileName);
byte[] downloaded = fileStorageService.download(fileName);
assertThat(downloaded).isEqualTo(content);
}
}Fang an zu üben!
Teste dein Wissen mit unseren Interview-Simulatoren und technischen Tests.
Best Practices und Performance-Optimierung
Eine effiziente Ausführung von Testcontainers erfordert einige Optimierungen, um die Build-Zeiten zu reduzieren.
@SpringBootTest
@Testcontainers
@ActiveProfiles("test")
public abstract class AbstractIntegrationTest {
// Shared container across all inheriting classes
@Container
@ServiceConnection
protected static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>(
DockerImageName.parse("postgres:16-alpine")
)
.withReuse(true);
@Autowired
protected JdbcTemplate jdbcTemplate;
@BeforeEach
void cleanDatabase() {
// Clean tables in order to respect FK constraints
jdbcTemplate.execute("TRUNCATE TABLE order_items CASCADE");
jdbcTemplate.execute("TRUNCATE TABLE orders CASCADE");
jdbcTemplate.execute("TRUNCATE TABLE users CASCADE");
}
}class UserIntegrationTest extends AbstractIntegrationTest {
@Autowired
private UserRepository userRepository;
@Test
void shouldCreateUser() {
// PostgreSQL container already started via parent class
User user = new User();
user.setEmail("inherited@test.com");
user.setName("Inherited Test");
User saved = userRepository.save(user);
assertThat(saved.getId()).isNotNull();
}
}Die abstrakte Klasse zentralisiert die Container-Konfiguration und die Datenbankbereinigung und vermeidet so Code-Duplizierung.
# src/test/resources/application-test.properties
# Test-specific configuration
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=false
# Disable Flyway/Liquibase if using ddl-auto
spring.flyway.enabled=false
# Reduced connection pool for tests
spring.datasource.hikari.maximum-pool-size=5
spring.datasource.hikari.minimum-idle=2Alpine-Images (postgres:16-alpine, redis:7-alpine) sind zu bevorzugen, da sie schlanker sind und schneller starten. Für Tests ist der funktionale Unterschied zu vollständigen Images vernachlässigbar.
Tests von Datenbankmigrationen
Testcontainers eignet sich hervorragend, um Flyway- oder Liquibase-Migrationen gegen eine echte Datenbank zu testen.
@DataJpaTest
@Testcontainers
@AutoConfigureTestDatabase(replace = Replace.NONE)
class FlywayMigrationTest {
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>(
DockerImageName.parse("postgres:16-alpine")
);
@Autowired
private Flyway flyway;
@Autowired
private JdbcTemplate jdbcTemplate;
@Test
void shouldApplyAllMigrations() {
// Given: migrations applied at startup
// When: check state
MigrationInfoService info = flyway.info();
// Then: all migrations are applied
assertThat(info.pending()).isEmpty();
assertThat(info.applied()).isNotEmpty();
}
@Test
void shouldCreateExpectedTables() {
// Given: migrations applied
// When: query system tables
List<String> tables = jdbcTemplate.queryForList(
"SELECT table_name FROM information_schema.tables " +
"WHERE table_schema = 'public' AND table_type = 'BASE TABLE'",
String.class
);
// Then: expected tables exist
assertThat(tables).contains("users", "orders", "order_items", "products");
}
@Test
void shouldHaveCorrectColumnTypes() {
// Given: users table created
// When: verify schema
List<Map<String, Object>> columns = jdbcTemplate.queryForList(
"SELECT column_name, data_type, is_nullable " +
"FROM information_schema.columns " +
"WHERE table_name = 'users'"
);
// Then: columns have correct types
assertThat(columns)
.extracting(c -> c.get("column_name"))
.contains("id", "email", "name", "created_at");
}
}Diese Tests stellen sicher, dass SQL-Migrationen vor dem Produktiv-Deployment korrekt funktionieren.
Fazit
Testcontainers verändert Spring-Boot-Integrationstests grundlegend, indem es sie zuverlässig, reproduzierbar und unabhängig von der lokalen Umgebung macht. Die native Unterstützung von Spring Boot 3.4 mit @ServiceConnection vereinfacht die Konfiguration erheblich, während die Container-Wiederverwendung die Ausführungszeiten optimiert.
Checkliste Testcontainers Spring Boot:
- ✅
spring-boot-testcontainersfür die native Auto-Konfiguration verwenden - ✅
@ServiceConnectiongegenüber@DynamicPropertySourcebevorzugen, wenn möglich - ✅
withReuse(true)aktivieren, um aufeinanderfolgende Ausführungen zu beschleunigen - ✅ Konfiguration in einer abstrakten Klasse oder
@TestConfigurationzentralisieren - ✅ Daten zwischen Tests mit
@BeforeEachoder@Sqlbereinigen - ✅ Alpine-Images für schnellere Startzeiten verwenden
- ✅ Flyway-/Liquibase-Migrationen gegen ein echtes PostgreSQL testen
- ✅ Docker Compose für Multi-Service-Umgebungen nutzen
Tags
Teilen
Verwandte Artikel

Spring Modulith: Modulare Monolith-Architektur erklärt
Spring Modulith lernen, um modulare Monolithen in Java zu bauen. Architektur, Module, asynchrone Events und Tests mit Spring Boot 3.

Spring Batch 5 Interview: Partitioning, Chunks und Fehlertoleranz
Meistern Sie Spring Batch 5 Interviews: 15 essenzielle Fragen zu Partitioning, Chunk-Verarbeitung und Fehlertoleranz mit Java 21 Codebeispielen.

Spring Boot Interview: Transaktions-Propagation erklärt
Beherrsche die Spring Boot Transaktions-Propagation: REQUIRED, REQUIRES_NEW, NESTED und mehr. 12 Interview-Fragen mit Code und typischen Fallstricken.