# Testcontainers Spring Boot: bezbolesne testy integracyjne > Kompletny przewodnik po konfiguracji Testcontainers ze Spring Boot 3.4. PostgreSQL, Redis i Kafka w kontenerach Docker dla niezawodnych i powtarzalnych testów integracyjnych. - Published: 2026-03-19 - Updated: 2026-05-01 - Author: SharpSkill - Tags: testcontainers, spring boot, integration testing, docker, postgresql - Reading time: 14 min --- Testy integracyjne stanowią poważne wyzwanie w rozwoju aplikacji Spring Boot. Testowanie wobec rzeczywistej bazy PostgreSQL lub brokera Kafka wymaga utrzymywania ciężkiej infrastruktury. Testcontainers rozwiązuje ten problem, uruchamiając kontenery Docker na żądanie podczas testów, gwarantując izolowane i powtarzalne środowiska. > **Spring Boot 3.4 i Testcontainers** > > Spring Boot 3.4 zawiera natywne wsparcie Testcontainers z autokonfiguracją. Zależności `spring-boot-testcontainers` drastycznie upraszczają instalację i umożliwiają ponowne wykorzystanie kontenerów między testami. ## Zrozumienie integracji między Testcontainers a Spring Boot Testcontainers udostępnia API Java do uruchamiania kontenerów Docker podczas wykonywania testów. Zamiast mockować zewnętrzne zależności lub utrzymywać współdzielone testowe bazy danych, każde uruchomienie testów otrzymuje własną odizolowaną instancję. Architektura opiera się na trzech głównych komponentach: bibliotece Testcontainers, która steruje Dockerem, wyspecjalizowanych modułach dla każdej technologii (PostgreSQL, Redis, Kafka) oraz integracji ze Spring Boot, która automatycznie wstrzykuje parametry połączenia. ```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 ``` BOM Testcontainers zapewnia spójność wersji wszystkich modułów wykorzystywanych w projekcie. ## Podstawowa konfiguracja z PostgreSQL Najczęstszym przypadkiem użycia jest testowanie wobec rzeczywistej bazy PostgreSQL. Spring Boot 3.4 oferuje dwa podejścia: adnotację `@ServiceConnection` do autokonfiguracji lub konfigurację ręczną przez `@DynamicPropertySource`. ```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"); } } ``` Adnotacja `@ServiceConnection` automatycznie wykrywa typ kontenera i konfiguruje odpowiednie właściwości Springa (`spring.datasource.url`, `spring.datasource.username` itd.). Takie podejście eliminuje powtarzalny kod konfiguracyjny. > **Cykl życia kontenera** > > Z adnotacją `@Container` na polu statycznym kontener uruchamia się raz przed wszystkimi testami w klasie i zatrzymuje po ostatnim teście. Aby uzyskać kontener na test, należy użyć niestatycznego pola instancyjnego. ## Ręczna konfiguracja z @DynamicPropertySource Niektóre scenariusze wymagają precyzyjniejszej kontroli nad wstrzykiwanymi właściwościami. Adnotacja `@DynamicPropertySource` pozwala jawnie zdefiniować wartości konfiguracyjne. ```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); } } ``` Skrypt inicjalizacyjny `withInitScript` przygotowuje schemat lub wstawia dane referencyjne przed wykonaniem testów. ## Pełne testy integracyjne Spring Boot Aby przetestować całą aplikację z załadowanymi wszystkimi komponentami, `@SpringBootTest` zastępuje `@DataJpaTest`. Ta konfiguracja uruchamia pełny kontekst Springa wraz z kontenerem PostgreSQL. ```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"); } } ``` Automatycznie skonfigurowany `TestRestTemplate` wskazuje na serwer uruchomiony na losowym porcie, co pozwala uniknąć konfliktów portów między równoległymi testami. ## Ponowne wykorzystanie kontenerów między testami Uruchomienie kontenera Docker zajmuje kilka sekund. Aby przyspieszyć wykonywanie testów, Spring Boot 3.4 umożliwia ponowne użycie kontenerów poprzez scentralizowaną konfigurację. ```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"); } } ``` Testy importują tę konfigurację, aby współdzielić te same kontenery. ```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); } } ``` Aby włączyć ponowne wykorzystanie kontenerów, należy dodać konfigurację w `~/.testcontainers.properties`: ```properties # ~/.testcontainers.properties testcontainers.reuse.enable=true ``` > **Czyszczenie danych** > > Przy ponownym użyciu kontenerów dane utrzymują się między uruchomieniami. Warto stosować `@Sql` lub `@BeforeEach`, aby czyścić tabele przed każdym testem, lub konfigurować inny schemat dla każdej klasy testowej. ## Testy z Redis i rozproszonym cache Testcontainers obsługuje Redis do testowania funkcjonalności pamięci podręcznej. Moduł Redis udostępnia preconfigurowany kontener gotowy do użycia. ```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 automatycznie wykrywa `RedisContainer` poprzez `@ServiceConnection` i konfiguruje `spring.data.redis.host` oraz `spring.data.redis.port`. ## Testy z Kafką i komunikatami asynchronicznymi Aplikacje sterowane zdarzeniami wymagają testów z rzeczywistym brokerem Kafka. Testcontainers udostępnia moduł Kafka, który uruchamia jednowęzłowy klaster odpowiedni do testów. ```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(); } } ``` Biblioteka Awaitility obsługuje asercje asynchroniczne z timeoutem, eliminując kruche wywołania `Thread.sleep` w testach. ## Konfiguracja wielokontenerowa z Docker Compose Dla złożonych aplikacji wymagających wielu współzależnych usług Testcontainers obsługuje pliki Docker Compose. ```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); } } ``` ## Dobre praktyki i optymalizacja wydajności Wydajne uruchamianie Testcontainers wymaga kilku optymalizacji w celu skrócenia czasu budowania. ```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(); } } ``` Klasa abstrakcyjna centralizuje konfigurację kontenera oraz czyszczenie bazy danych, eliminując duplikację kodu. ```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 ``` > **Lekkie obrazy Docker** > > Warto preferować obrazy Alpine (`postgres:16-alpine`, `redis:7-alpine`), które są lżejsze i szybciej startują. W testach różnica funkcjonalna względem pełnych obrazów jest pomijalna. ## Testy migracji bazy danych Testcontainers świetnie nadaje się do testowania migracji Flyway lub Liquibase wobec rzeczywistej bazy danych. ```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> 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"); } } ``` Takie testy gwarantują, że migracje SQL działają poprawnie przed wdrożeniem na produkcję. ## Podsumowanie Testcontainers przekształca testy integracyjne Spring Boot, czyniąc je niezawodnymi, powtarzalnymi i niezależnymi od środowiska lokalnego. Natywne wsparcie Spring Boot 3.4 z `@ServiceConnection` znacząco upraszcza konfigurację, a ponowne wykorzystanie kontenerów optymalizuje czas wykonania. **Lista kontrolna Testcontainers Spring Boot:** - ✅ Używać `spring-boot-testcontainers` do natywnej autokonfiguracji - ✅ Preferować `@ServiceConnection` zamiast `@DynamicPropertySource`, gdy to możliwe - ✅ Włączać `withReuse(true)`, aby przyspieszyć kolejne uruchomienia - ✅ Centralizować konfigurację w klasie abstrakcyjnej lub `@TestConfiguration` - ✅ Czyścić dane między testami za pomocą `@BeforeEach` lub `@Sql` - ✅ Stosować obrazy Alpine dla szybszego startu - ✅ Testować migracje Flyway/Liquibase wobec rzeczywistego PostgreSQL - ✅ Wykorzystywać Docker Compose dla środowisk wieloserwisowych --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/pl/blog/spring-boot/testcontainers-spring-boot-integration-testing