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.

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 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.
<!-- pom.xml -->
<dependencies>
<!-- Dépendance principale Testcontainers -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
<!-- Module PostgreSQL -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
<!-- Support JUnit 5 -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<!-- BOM Testcontainers pour gérer les versions -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-bom</artifactId>
<version>1.20.4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>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.
@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<User> 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<User> 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.
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.
@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.
@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<UserResponse> 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<UserResponse> 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.
Prêt à réussir tes entretiens Spring Boot ?
Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.
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.
@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.
@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 :
# ~/.testcontainers.properties
testcontainers.reuse.enable=trueAvec 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.
@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.
@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 : 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");
});
}
}@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();
}
}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.
# 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) {
// 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);
}
}Passe à la pratique !
Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.
Bonnes pratiques et optimisation des performances
L'exécution efficace des tests Testcontainers nécessite quelques optimisations pour réduire les temps de build.
@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");
}
}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.
# 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=2Pré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.
@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<String> 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<Map<String, Object>> 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-testcontainerspour l'auto-configuration native - ✅ Préférer
@ServiceConnectionà@DynamicPropertySourcequand 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
@BeforeEachou@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
Tags
Partager
Articles similaires

Spring Modulith : architecture modulaire monolithique expliquée
Découvrez Spring Modulith pour construire des monolithes modulaires en Java. Architecture, modules, événements asynchrones et tests avec exemples Spring Boot 3.

Spring Batch 5 en entretien technique : partitioning, chunks et fault tolerance
Préparez vos entretiens Spring Batch 5 : 15 questions essentielles sur le partitioning, chunk-oriented processing, fault tolerance avec exemples de code Java 21.

Questions entretien Spring Boot : propagation des transactions expliquée
Maîtrisez la propagation des transactions Spring Boot : REQUIRED, REQUIRES_NEW, NESTED et plus. 12 questions d'entretien avec exemples de code et pièges courants.