# Testcontainers Spring Boot: pijnloze integratietests > Volledige gids voor het configureren van Testcontainers met Spring Boot 3.4. PostgreSQL, Redis en Kafka in Docker-containers voor betrouwbare, reproduceerbare integratietests. - Published: 2026-03-19 - Updated: 2026-05-01 - Author: SharpSkill - Tags: testcontainers, spring boot, integration testing, docker, postgresql - Reading time: 14 min --- Integratietests vormen een grote uitdaging in de ontwikkeling van Spring Boot-applicaties. Testen tegen een echte PostgreSQL-database of Kafka-broker vereist het onderhouden van zware infrastructuur. Testcontainers lost dit probleem op door tijdens de tests Docker-containers op aanvraag te starten, wat geïsoleerde en reproduceerbare omgevingen garandeert. > **Spring Boot 3.4 en Testcontainers** > > Spring Boot 3.4 bevat native Testcontainers-ondersteuning met auto-configuratie. De afhankelijkheden `spring-boot-testcontainers` vereenvoudigen de installatie drastisch en maken hergebruik van containers tussen tests mogelijk. ## De integratie tussen Testcontainers en Spring Boot begrijpen Testcontainers biedt een Java-API om Docker-containers te starten tijdens de uitvoering van tests. In plaats van externe afhankelijkheden te mocken of gedeelde testdatabases te onderhouden, krijgt elke testrun zijn eigen geïsoleerde instantie. De architectuur steunt op drie hoofdcomponenten: de Testcontainers-bibliotheek die Docker aanstuurt, gespecialiseerde modules voor elke technologie (PostgreSQL, Redis, Kafka) en de Spring Boot-integratie die automatisch de verbindingsparameters injecteert. ```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 ``` De Testcontainers BOM zorgt voor versieconsistentie tussen alle modules die in het project worden gebruikt. ## Basisconfiguratie met PostgreSQL Het meest voorkomende scenario betreft testen tegen een echte PostgreSQL-database. Spring Boot 3.4 biedt twee benaderingen: de annotatie `@ServiceConnection` voor auto-configuratie of handmatige configuratie via `@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"); } } ``` De annotatie `@ServiceConnection` detecteert automatisch het containertype en configureert de bijbehorende Spring-properties (`spring.datasource.url`, `spring.datasource.username`, enzovoort). Deze aanpak elimineert herhalende configuratiecode. > **Levenscyclus van de container** > > Met `@Container` op een statisch veld start de container eenmalig vóór alle tests in de klasse en stopt deze na de laatste test. Voor een container per test moet een niet-statisch instantieveld worden gebruikt. ## Handmatige configuratie met @DynamicPropertySource Sommige scenario's vereisen fijnere controle over de geïnjecteerde properties. De annotatie `@DynamicPropertySource` maakt het mogelijk configuratiewaarden expliciet te definiëren. ```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); } } ``` Het initialisatiescript `withInitScript` bereidt het schema voor of voegt referentiedata toe vóór de uitvoering van de tests. ## Volledige Spring Boot-integratietests Om de volledige applicatie te testen met alle componenten geladen, vervangt `@SpringBootTest` de annotatie `@DataJpaTest`. Deze configuratie start de volledige Spring-context samen met de PostgreSQL-container. ```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"); } } ``` De automatisch geconfigureerde `TestRestTemplate` wijst naar de server die op een willekeurige poort is gestart, waardoor poortconflicten tussen parallelle tests worden vermeden. ## Hergebruik van containers tussen tests Het starten van een Docker-container duurt enkele seconden. Om de uitvoering van tests te versnellen, maakt Spring Boot 3.4 hergebruik van containers mogelijk via een gecentraliseerde configuratie. ```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"); } } ``` De tests importeren deze configuratie om dezelfde containers te delen. ```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); } } ``` Om hergebruik van containers in te schakelen, moet de configuratie worden toegevoegd in `~/.testcontainers.properties`: ```properties # ~/.testcontainers.properties testcontainers.reuse.enable=true ``` > **Data opschonen** > > Bij hergebruik van containers blijven de data bestaan tussen uitvoeringen. Het is raadzaam `@Sql` of `@BeforeEach` te gebruiken om tabellen vóór elke test op te schonen, of een ander schema per testklasse te configureren. ## Tests met Redis en gedistribueerde cache Testcontainers ondersteunt Redis voor het testen van cachefunctionaliteit. De Redis-module biedt een vooraf geconfigureerde container die klaar is voor gebruik. ```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 detecteert de `RedisContainer` automatisch via `@ServiceConnection` en configureert `spring.data.redis.host` en `spring.data.redis.port`. ## Tests met Kafka en asynchrone messaging Event-driven applicaties vereisen tests met een echte Kafka-broker. Testcontainers biedt een Kafka-module die een single-node cluster start die geschikt is voor tests. ```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(); } } ``` De Awaitility-bibliotheek behandelt asynchrone asserties met timeout en vermijdt zo fragiele `Thread.sleep`-aanroepen in tests. ## Multi-container configuratie met Docker Compose Voor complexe applicaties die meerdere onderling afhankelijke services vereisen, ondersteunt Testcontainers Docker Compose-bestanden. ```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); } } ``` ## Best practices en prestatie-optimalisatie Een efficiënte uitvoering van Testcontainers vereist enkele optimalisaties om de buildtijden te verkorten. ```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(); } } ``` De abstracte klasse centraliseert de containerconfiguratie en het opschonen van de database, waardoor codeduplicatie wordt voorkomen. ```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 ``` > **Lichte Docker-images** > > Alpine-images (`postgres:16-alpine`, `redis:7-alpine`) verdienen de voorkeur omdat ze lichter zijn en sneller starten. Voor tests is het functionele verschil ten opzichte van de volledige images verwaarloosbaar. ## Tests van databasemigraties Testcontainers blinkt uit in het testen van Flyway- of Liquibase-migraties tegen een echte database. ```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"); } } ``` Deze tests garanderen dat SQL-migraties correct werken vóór de productie-deployment. ## Conclusie Testcontainers transformeert Spring Boot-integratietests door ze betrouwbaar, reproduceerbaar en onafhankelijk van de lokale omgeving te maken. De native ondersteuning van Spring Boot 3.4 met `@ServiceConnection` vereenvoudigt de configuratie aanzienlijk, terwijl het hergebruik van containers de uitvoeringstijden optimaliseert. **Checklist Testcontainers Spring Boot:** - ✅ Gebruik `spring-boot-testcontainers` voor de native auto-configuratie - ✅ Geef voorkeur aan `@ServiceConnection` boven `@DynamicPropertySource` waar mogelijk - ✅ Activeer `withReuse(true)` om opeenvolgende uitvoeringen te versnellen - ✅ Centraliseer de configuratie in een abstracte klasse of `@TestConfiguration` - ✅ Schoon data tussen tests op met `@BeforeEach` of `@Sql` - ✅ Gebruik Alpine-images voor snellere starttijden - ✅ Test Flyway-/Liquibase-migraties tegen een echte PostgreSQL - ✅ Maak gebruik van Docker Compose voor multi-service omgevingen --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/nl/blog/spring-boot/testcontainers-spring-boot-integration-testing