# Testcontainers Spring Boot: sancısız entegrasyon testleri
> Spring Boot 3.4 ile Testcontainers yapılandırması için eksiksiz rehber. Güvenilir ve tekrarlanabilir entegrasyon testleri için Docker konteynerlerinde PostgreSQL, Redis ve Kafka.
- Published: 2026-03-19
- Updated: 2026-05-01
- Author: SharpSkill
- Tags: testcontainers, spring boot, integration testing, docker, postgresql
- Reading time: 14 min
---
Entegrasyon testleri, Spring Boot uygulama geliştirmede önemli bir zorluk oluşturur. Gerçek bir PostgreSQL veritabanı veya Kafka broker'ı üzerinde test yapmak ağır bir altyapının sürdürülmesini gerektirir. Testcontainers bu sorunu, testler sırasında talep üzerine Docker konteynerleri başlatarak çözer ve izole, tekrarlanabilir ortamlar garanti eder.
> **Spring Boot 3.4 ve Testcontainers**
>
> Spring Boot 3.4, otomatik yapılandırma ile yerel Testcontainers desteği içerir. `spring-boot-testcontainers` bağımlılıkları kurulumu büyük ölçüde basitleştirir ve testler arasında konteyner yeniden kullanımına olanak tanır.
## Testcontainers ve Spring Boot entegrasyonunu anlamak
Testcontainers, test yürütme sırasında Docker konteynerleri başlatmak için bir Java API sunar. Dış bağımlılıkları mocklamak ya da paylaşılan test veritabanlarını sürdürmek yerine, her test çalıştırması kendi izole örneğini elde eder.
Mimari üç ana bileşene dayanır: Docker'ı yöneten Testcontainers kütüphanesi, her teknoloji için özelleşmiş modüller (PostgreSQL, Redis, Kafka) ve bağlantı parametrelerini otomatik olarak enjekte eden Spring Boot entegrasyonu.
```xml
org.springframework.boot
spring-boot-testcontainers
test
org.testcontainers
postgresql
test
org.testcontainers
junit-jupiter
test
org.testcontainers
testcontainers-bom
1.20.4
pom
import
```
Testcontainers BOM'u, projede kullanılan tüm modüller arasında sürüm tutarlılığı sağlar.
## PostgreSQL ile temel yapılandırma
En yaygın kullanım senaryosu, gerçek bir PostgreSQL veritabanına karşı test yapmaktır. Spring Boot 3.4 iki yaklaşım sunar: otomatik yapılandırma için `@ServiceConnection` anotasyonu veya `@DynamicPropertySource` aracılığıyla manuel yapılandırma.
```java
// UserRepositoryIntegrationTest.java
@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 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 found = userRepository.findByEmail("search@example.com");
// Then: user is found
assertThat(found).isPresent();
assertThat(found.get().getName()).isEqualTo("Search User");
}
}
```
`@ServiceConnection` anotasyonu konteyner türünü otomatik olarak algılar ve karşılık gelen Spring özelliklerini (`spring.datasource.url`, `spring.datasource.username` vb.) yapılandırır. Bu yaklaşım, tekrarlayan yapılandırma kodunu ortadan kaldırır.
> **Konteyner yaşam döngüsü**
>
> Statik bir alanda `@Container` ile konteyner, sınıfın tüm testlerinden önce bir kez başlar ve son testten sonra durur. Test başına bir konteyner için, statik olmayan bir örnek alanı kullanılmalıdır.
## @DynamicPropertySource ile manuel yapılandırma
Bazı senaryolar enjekte edilen özellikler üzerinde daha ince kontrol gerektirir. `@DynamicPropertySource` anotasyonu, yapılandırma değerlerinin açıkça tanımlanmasına olanak tanır.
```java
// OrderRepositoryIntegrationTest.java
@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);
}
}
```
`withInitScript` başlatma betiği, test yürütülmeden önce şemayı hazırlar veya referans verileri ekler.
## Tam Spring Boot entegrasyon testleri
Tüm bileşenler yüklenmiş şekilde uygulamanın tamamını test etmek için `@SpringBootTest`, `@DataJpaTest` yerine geçer. Bu yapılandırma, tam Spring bağlamını PostgreSQL konteyneri ile birlikte başlatır.
```java
// UserServiceIntegrationTest.java
@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 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 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");
}
}
```
Otomatik yapılandırılan `TestRestTemplate`, rastgele bir portta başlatılan sunucuyu hedef alır ve paralel testler arasında port çakışmalarını önler.
## Testler arasında konteyner yeniden kullanımı
Bir Docker konteyneri başlatmak birkaç saniye sürer. Test yürütmesini hızlandırmak için Spring Boot 3.4, merkezi bir yapılandırma aracılığıyla konteyner yeniden kullanımına olanak tanır.
```java
// TestcontainersConfiguration.java
@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");
}
}
```
Testler aynı konteynerleri paylaşmak için bu yapılandırmayı içe aktarır.
```java
// ProductServiceIntegrationTest.java
@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);
}
}
```
Konteyner yeniden kullanımını etkinleştirmek için `~/.testcontainers.properties` dosyasına yapılandırma eklenmelidir:
```properties
# ~/.testcontainers.properties
testcontainers.reuse.enable=true
```
> **Veri temizleme**
>
> Konteyner yeniden kullanımıyla, çalıştırmalar arasında veriler kalır. Her testten önce tabloları temizlemek için `@Sql` veya `@BeforeEach` kullanılması ya da test sınıfı başına farklı bir şema yapılandırılması önerilir.
## Redis ve dağıtık önbellek ile testler
Testcontainers, önbellek işlevlerini test etmek için Redis'i destekler. Redis modülü, kullanıma hazır önceden yapılandırılmış bir konteyner sağlar.
```java
// CacheServiceIntegrationTest.java
@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, `@ServiceConnection` aracılığıyla `RedisContainer`'ı otomatik algılar ve `spring.data.redis.host` ile `spring.data.redis.port`'u yapılandırır.
## Kafka ve asenkron mesajlaşma ile testler
Olay tabanlı uygulamalar gerçek bir Kafka broker'ı ile test gerektirir. Testcontainers, testlere uygun tek düğümlü bir küme başlatan bir Kafka modülü sağlar.
```java
// OrderEventIntegrationTest.java
@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 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");
});
}
}
```
```java
// OrderEventConsumer.java
@Component
public class OrderEventConsumer {
private final List receivedEvents = new CopyOnWriteArrayList<>();
@KafkaListener(topics = "order-events", groupId = "test-group")
public void consume(OrderEvent event) {
receivedEvents.add(event);
}
public List getReceivedEvents() {
return List.copyOf(receivedEvents);
}
public void clear() {
receivedEvents.clear();
}
}
```
Awaitility kütüphanesi, testlerde kırılgan `Thread.sleep` çağrılarını önleyerek zaman aşımı ile asenkron iddiaları yönetir.
## Docker Compose ile çoklu konteyner yapılandırması
Birbirine bağlı birden fazla servis gerektiren karmaşık uygulamalar için Testcontainers, Docker Compose dosyalarını destekler.
```yaml
# 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"
```
```java
// FullStackIntegrationTest.java
@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);
}
}
```
## En iyi uygulamalar ve performans optimizasyonu
Testcontainers'ın verimli yürütülmesi, derleme sürelerini azaltmak için bazı optimizasyonlar gerektirir.
```java
// AbstractIntegrationTest.java
@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");
}
}
```
```java
// UserIntegrationTest.java
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();
}
}
```
Soyut sınıf, konteyner yapılandırmasını ve veritabanı temizliğini merkezileştirerek kod yinelenmesini önler.
```properties
# 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=2
```
> **Hafif Docker imajları**
>
> Daha hafif olan ve daha hızlı başlayan Alpine imajları (`postgres:16-alpine`, `redis:7-alpine`) tercih edilmelidir. Testlerde tam imajlardan işlevsel fark ihmal edilebilir düzeydedir.
## Veritabanı geçişlerinin testleri
Testcontainers, gerçek bir veritabanına karşı Flyway veya Liquibase geçişlerini test etmede başarılıdır.
```java
// FlywayMigrationTest.java
@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 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