# Testcontainers avec Spring Boot : tests d'intégration sans douleur > Guide complet pour configurer Testcontainers avec Spring Boot 3.4. PostgreSQL, Redis, Kafka en containers Docker pour des tests d'intégration fiables et reproductibles. - Published: 2026-03-19 - Updated: 2026-03-31 - Author: SharpSkill - Tags: testcontainers, spring boot, tests intégration, docker, postgresql - Reading time: 14 min --- Les tests d'intégration représentent un défi majeur dans le développement d'applications Spring Boot. Tester contre une vraie base de données PostgreSQL ou un broker Kafka nécessite une infrastructure lourde à maintenir. Testcontainers résout ce problème en démarrant des containers Docker à la demande pendant les tests, garantissant des environnements isolés et reproductibles. > **Spring Boot 3.4 et Testcontainers** > > Spring Boot 3.4 intègre un support natif de Testcontainers avec auto-configuration. Les dépendances `spring-boot-testcontainers` simplifient drastiquement la configuration et permettent de réutiliser les containers entre les tests. ## Comprendre Testcontainers et son intégration Spring Boot Testcontainers fournit une API Java pour démarrer des containers Docker pendant l'exécution des tests. Au lieu de mocker les dépendances externes ou de maintenir des bases de données de test partagées, chaque exécution de test dispose de sa propre instance isolée. L'architecture repose sur trois composants principaux : la bibliothèque Testcontainers qui pilote Docker, les modules spécialisés pour chaque technologie (PostgreSQL, Redis, Kafka), et l'intégration Spring Boot qui injecte automatiquement les paramètres de connexion. ```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 ``` Le BOM Testcontainers garantit la cohérence des versions entre tous les modules utilisés dans le projet. ## Configuration de base avec PostgreSQL Le cas d'usage le plus courant concerne les tests avec une vraie base de données PostgreSQL. Spring Boot 3.4 offre deux approches : l'annotation `@ServiceConnection` pour l'auto-configuration, ou la configuration manuelle via `@DynamicPropertySource`. ```java // UserRepositoryIntegrationTest.java @DataJpaTest @Testcontainers @AutoConfigureTestDatabase(replace = Replace.NONE) class UserRepositoryIntegrationTest { // Démarre un container PostgreSQL avant les tests @Container @ServiceConnection static PostgreSQLContainer postgres = new PostgreSQLContainer<>( DockerImageName.parse("postgres:16-alpine") ); @Autowired private UserRepository userRepository; @Test void shouldSaveAndRetrieveUser() { // Given : un utilisateur à persister User user = new User(); user.setEmail("test@example.com"); user.setName("Test User"); // When : sauvegarde et récupération User saved = userRepository.save(user); Optional found = userRepository.findById(saved.getId()); // Then : l'utilisateur est correctement persisté assertThat(found).isPresent(); assertThat(found.get().getEmail()).isEqualTo("test@example.com"); } @Test void shouldFindUserByEmail() { // Given : un utilisateur en base User user = new User(); user.setEmail("search@example.com"); user.setName("Search User"); userRepository.save(user); // When : recherche par email Optional found = userRepository.findByEmail("search@example.com"); // Then : l'utilisateur est trouvé assertThat(found).isPresent(); assertThat(found.get().getName()).isEqualTo("Search User"); } } ``` L'annotation `@ServiceConnection` détecte automatiquement le type de container et configure les propriétés Spring correspondantes (`spring.datasource.url`, `spring.datasource.username`, etc.). Cette approche élimine le code de configuration boilerplate. > **Container lifecycle** > > Avec `@Container` sur un champ static, le container démarre une fois avant tous les tests de la classe et s'arrête après le dernier test. Pour un container par test, utiliser un champ d'instance non-static. ## Configuration manuelle avec @DynamicPropertySource Certains scénarios nécessitent un contrôle plus fin sur les propriétés injectées. L'annotation `@DynamicPropertySource` permet de définir explicitement les valeurs de configuration. ```java // OrderRepositoryIntegrationTest.java @DataJpaTest @Testcontainers @AutoConfigureTestDatabase(replace = Replace.NONE) class OrderRepositoryIntegrationTest { @Container static PostgreSQLContainer postgres = new PostgreSQLContainer<>( DockerImageName.parse("postgres:16-alpine") ) // Configuration spécifique du container .withDatabaseName("orders_test") .withUsername("test_user") .withPassword("test_password") // Script d'initialisation SQL .withInitScript("db/init-orders.sql"); // Injection manuelle des propriétés dynamiques @DynamicPropertySource static void configureProperties(DynamicPropertyRegistry registry) { // URL JDBC générée dynamiquement avec le port mappé registry.add("spring.datasource.url", postgres::getJdbcUrl); registry.add("spring.datasource.username", postgres::getUsername); registry.add("spring.datasource.password", postgres::getPassword); // Propriétés additionnelles si nécessaire registry.add("spring.jpa.hibernate.ddl-auto", () -> "validate"); } @Autowired private OrderRepository orderRepository; @Autowired private EntityManager entityManager; @Test void shouldPersistOrderWithItems() { // Given : une commande avec des 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 : sauvegarde de la commande Order saved = orderRepository.save(order); entityManager.flush(); entityManager.clear(); // Then : la commande et ses items sont persistés Order found = orderRepository.findById(saved.getId()).orElseThrow(); assertThat(found.getItems()).hasSize(1); assertThat(found.getItems().get(0).getQuantity()).isEqualTo(2); } } ``` Le script d'initialisation `withInitScript` permet de préparer le schéma ou d'insérer des données de référence avant l'exécution des tests. ## Tests d'intégration avec Spring Boot complet Pour tester l'application dans son ensemble avec tous les composants chargés, `@SpringBootTest` remplace `@DataJpaTest`. Cette configuration démarre le contexte Spring complet avec le container 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 : une requête de création CreateUserRequest request = new CreateUserRequest( "api@example.com", "API User", "securePassword123" ); // When : appel de l'API REST ResponseEntity response = restTemplate.postForEntity( "/api/users", request, UserResponse.class ); // Then : l'utilisateur est créé avec succès assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); assertThat(response.getBody()).isNotNull(); assertThat(response.getBody().email()).isEqualTo("api@example.com"); } @Test void shouldRetrieveUserById() { // Given : un utilisateur existant UserResponse created = userService.createUser( new CreateUserRequest("retrieve@example.com", "Retrieve User", "password") ); // When : récupération par ID ResponseEntity response = restTemplate.getForEntity( "/api/users/" + created.id(), UserResponse.class ); // Then : l'utilisateur est retourné assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(response.getBody().name()).isEqualTo("Retrieve User"); } } ``` Le `TestRestTemplate` automatiquement configuré pointe vers le serveur démarré sur un port aléatoire, évitant les conflits de ports entre tests parallèles. ## Réutilisation des containers entre tests Le démarrage d'un container Docker prend plusieurs secondes. Pour accélérer l'exécution des tests, Spring Boot 3.4 permet de réutiliser les containers via une configuration centralisée. ```java // TestcontainersConfiguration.java @TestConfiguration(proxyBeanMethods = false) public class TestcontainersConfiguration { // Bean container réutilisable entre tous les 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"); } } ``` Les tests importent cette configuration pour partager les mêmes containers. ```java // ProductServiceIntegrationTest.java @SpringBootTest @Import(TestcontainersConfiguration.class) class ProductServiceIntegrationTest { @Autowired private ProductService productService; @Autowired private ProductRepository productRepository; @Test void shouldCacheProductDetails() { // Given : un produit en base Product product = new Product(); product.setName("Cached Product"); product.setPrice(BigDecimal.valueOf(99.99)); productRepository.save(product); // When : deux appels successifs ProductDto first = productService.getProductById(product.getId()); ProductDto second = productService.getProductById(product.getId()); // Then : le deuxième appel utilise le cache assertThat(first).isEqualTo(second); } } ``` Pour activer la réutilisation des containers, ajouter la configuration dans `~/.testcontainers.properties` : ```properties # ~/.testcontainers.properties testcontainers.reuse.enable=true ``` > **Nettoyage des données** > > Avec la réutilisation des containers, les données persistent entre les exécutions. Utiliser `@Sql` ou `@BeforeEach` pour nettoyer les tables avant chaque test, ou configurer un schéma différent par classe de test. ## Tests avec Redis et cache distribué Testcontainers supporte Redis pour tester les fonctionnalités de cache. Le module Redis fournit un container préconfigured prêt à l'emploi. ```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 : une valeur à mettre en cache String key = "user:123"; String value = "{\"id\":123,\"name\":\"Cached User\"}"; // When : stockage en cache cacheService.put(key, value, Duration.ofMinutes(10)); // Then : la valeur est récupérable String cached = cacheService.get(key); assertThat(cached).isEqualTo(value); } @Test void shouldExpireAfterTtl() throws InterruptedException { // Given : une valeur avec TTL court String key = "expiring:key"; cacheService.put(key, "temporary", Duration.ofSeconds(1)); // When : attente de l'expiration Thread.sleep(1500); // Then : la clé a expiré String cached = cacheService.get(key); assertThat(cached).isNull(); } @Test void shouldIncrementCounter() { // Given : une clé de compteur String counterKey = "page:views:homepage"; // When : incrémentations multiples Long first = redisTemplate.opsForValue().increment(counterKey); Long second = redisTemplate.opsForValue().increment(counterKey); Long third = redisTemplate.opsForValue().increment(counterKey); // Then : le compteur s'incrémente correctement assertThat(first).isEqualTo(1); assertThat(second).isEqualTo(2); assertThat(third).isEqualTo(3); } } ``` Spring Boot détecte automatiquement le `RedisContainer` grâce à `@ServiceConnection` et configure `spring.data.redis.host` et `spring.data.redis.port`. ## Tests avec Kafka et messaging asynchrone Les applications event-driven nécessitent des tests avec un broker Kafka réel. Testcontainers fournit un module Kafka qui démarre un cluster single-node adapté aux 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 : un événement de commande OrderEvent event = new OrderEvent( "ORD-2026-100", OrderEventType.CREATED, LocalDateTime.now() ); // When : publication sur Kafka kafkaTemplate.send("order-events", event.orderId(), event).get(); // Then : l'événement est consommé (avec 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(); } } ``` La bibliothèque Awaitility gère les assertions asynchrones avec timeout, évitant les `Thread.sleep` fragiles dans les tests. ## Configuration multi-container avec Docker Compose Pour les applications complexes nécessitant plusieurs services interdépendants, Testcontainers supporte les fichiers 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) { // Configuration PostgreSQL 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"); // Configuration Redis 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); // Configuration LocalStack S3 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 : un fichier à uploader byte[] content = "Test file content".getBytes(); String fileName = "test-file.txt"; // When : upload vers S3 via LocalStack String url = fileStorageService.upload(fileName, content); // Then : le fichier est accessible assertThat(url).contains(fileName); byte[] downloaded = fileStorageService.download(fileName); assertThat(downloaded).isEqualTo(content); } } ``` ## Bonnes pratiques et optimisation des performances L'exécution efficace des tests Testcontainers nécessite quelques optimisations pour réduire les temps de build. ```java // AbstractIntegrationTest.java @SpringBootTest @Testcontainers @ActiveProfiles("test") public abstract class AbstractIntegrationTest { // Container partagé entre toutes les classes héritant @Container @ServiceConnection protected static PostgreSQLContainer postgres = new PostgreSQLContainer<>( DockerImageName.parse("postgres:16-alpine") ) .withReuse(true); @Autowired protected JdbcTemplate jdbcTemplate; @BeforeEach void cleanDatabase() { // Nettoyage des tables dans l'ordre pour respecter les FK 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() { // Le container PostgreSQL est déjà démarré via la classe parent User user = new User(); user.setEmail("inherited@test.com"); user.setName("Inherited Test"); User saved = userRepository.save(user); assertThat(saved.getId()).isNotNull(); } } ``` La classe abstraite centralise la configuration des containers et le nettoyage de la base de données, évitant la duplication de code. ```properties # src/test/resources/application-test.properties # Configuration spécifique aux tests spring.jpa.hibernate.ddl-auto=create-drop spring.jpa.show-sql=false # Désactiver Flyway/Liquibase si utilisation de ddl-auto spring.flyway.enabled=false # Pool de connexions réduit pour les tests spring.datasource.hikari.maximum-pool-size=5 spring.datasource.hikari.minimum-idle=2 ``` > **Images Docker légères** > > Préférer les images Alpine (`postgres:16-alpine`, `redis:7-alpine`) qui sont plus légères et démarrent plus rapidement. Pour les tests, la différence de fonctionnalités avec les images complètes est négligeable. ## Tests de migration de base de données Testcontainers excelle pour tester les migrations Flyway ou Liquibase contre une vraie base de données. ```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 appliquées au démarrage // When : vérification de l'état MigrationInfoService info = flyway.info(); // Then : toutes les migrations sont appliquées assertThat(info.pending()).isEmpty(); assertThat(info.applied()).isNotEmpty(); } @Test void shouldCreateExpectedTables() { // Given : migrations appliquées // When : requête des tables système List tables = jdbcTemplate.queryForList( "SELECT table_name FROM information_schema.tables " + "WHERE table_schema = 'public' AND table_type = 'BASE TABLE'", String.class ); // Then : les tables attendues existent assertThat(tables).contains("users", "orders", "order_items", "products"); } @Test void shouldHaveCorrectColumnTypes() { // Given : table users créée // When : vérification du schéma List> columns = jdbcTemplate.queryForList( "SELECT column_name, data_type, is_nullable " + "FROM information_schema.columns " + "WHERE table_name = 'users'" ); // Then : les colonnes ont les bons types assertThat(columns) .extracting(c -> c.get("column_name")) .contains("id", "email", "name", "created_at"); } } ``` Ces tests garantissent que les migrations SQL fonctionnent correctement avant le déploiement en production. ## Conclusion Testcontainers transforme les tests d'intégration Spring Boot en les rendant fiables, reproductibles et indépendants de l'environnement local. Le support natif de Spring Boot 3.4 avec `@ServiceConnection` simplifie considérablement la configuration, tandis que la réutilisation des containers optimise les temps d'exécution. **Checklist Testcontainers Spring Boot :** - ✅ Utiliser `spring-boot-testcontainers` pour l'auto-configuration native - ✅ Préférer `@ServiceConnection` à `@DynamicPropertySource` quand possible - ✅ Activer `withReuse(true)` pour accélérer les exécutions successives - ✅ Centraliser la configuration dans une classe abstraite ou `@TestConfiguration` - ✅ Nettoyer les données entre tests avec `@BeforeEach` ou `@Sql` - ✅ Utiliser des images Alpine pour des démarrages plus rapides - ✅ Tester les migrations Flyway/Liquibase contre un vrai PostgreSQL - ✅ Exploiter Docker Compose pour les environnements multi-services --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/fr/blog/spring-boot/testcontainers-spring-boot-tests-integration