Skip to Content
BackendTestcontainers

Testcontainers

Lancer des conteneurs Docker éphémères dans les tests d’intégration Spring Boot — bases de données, files d’attente, caches, serveurs externes.

L’idée : au lieu de mocker les dépendances externes, les tester avec une vraie instance. Un conteneur Docker démarrera au début des tests, sera détruit à la fin.

Dépendances

Maven

<!-- Essentiel : junit-jupiter et spring-boot-test pour la règle --> <dependency> <groupId>org.testcontainers</groupId> <artifactId>testcontainers</artifactId> <version>1.20.4</version> <scope>test</scope> </dependency> <!-- Extensions : uniquement celles que l'on utilise --> <dependency> <groupId>org.testcontainers</groupId> <artifactId>junit-jupiter</artifactId> <version>1.20.4</version> <scope>test</scope> </dependency> <dependency> <groupId>org.testcontainers</groupId> <artifactId>postgresql</artifactId> <version>1.20.4</version> <scope>test</scope> </dependency> <dependency> <groupId>org.testcontainers</groupId> <artifactId>redis</artifactId> <version>1.20.4</version> <scope>test</scope> </dependency>

Gradle

testImplementation platform('org.testcontainers:testcontainers-bom:1.20.4') testImplementation 'org.testcontainers:testcontainers' testImplementation 'org.testcontainers:junit-jupiter' // Seulement si nécessaire testImplementation 'org.testcontainers:postgresql' testImplementation 'org.testcontainers:redis'

Note : junit-jupiter apporte la règle Testcontainers qui gère le cycle de vie des conteneurs. testcontainers seul fournit les classes de base (GenericContainer, ImagePullPolicy, …).

Règle JUnit 5 : Testcontainers

Déclarer un conteneur statique et gérer leur cycle de vie via la règle JUnit 5 :

import org.testcontainers.containers.PostgreSQLContainer; import org.junit.jupiter.api.Testcontainers; import org.junit.jupiter.api.Test; import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest @Testcontainers class MaClasseTest { @Container static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine") .withDatabaseName("testdb") .withUsername("test") .withPassword("test"); @DynamicPropertySource static void configurerProperties(DynamicPropertyRegistry registry) { registry.add("spring.datasource.url", postgres::getJdbcUrl); registry.add("spring.datasource.username", postgres::getUsername); registry.add("spring.datasource.password", postgres::getPassword); } @Test void requeteFonctionneAvecVraiPostgres() { // ... utiliser la vraie base } }

Piège : le conteneur doit être static (une seule instance pour toute la classe de test) et @Container (la règle JUnit 5 le détecte et le lance/détruit). Un conteneur non static sera détruit après chaque méthode, ce qui annule tout bénéfice.

Piège : @Testcontainers est obligatoire. Sans cette annotation, la règle JUnit 5 ne scannera pas les champs @Container. Les conteneurs seront alors null au runtime.

Exemples de conteneurs

PostgreSQL

PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine") .withDatabaseName("testdb") .withUsername("test") .withPassword("test") .withInitScript("schema.sql"); // exécute un script SQL au premier démarrage

Variables d’environnement à injecter via DynamicPropertySource :

Clé SpringValeur registry.add(...)
spring.datasource.urlpostgres::getJdbcUrl
spring.datasource.usernamepostgres::getUsername
spring.datasource.passwordpostgres::getPassword
spring.datasource.driver-class-nameconstante org.postgresql.Driver

Redis

GenericContainer<?> redis = new GenericContainer<>("redis:7-alpine") .withExposedPorts(6379); @DynamicPropertySource static void configurer(DynamicPropertyRegistry registry) { registry.add("spring.data.redis.host", redis::getHost); registry.add("spring.data.redis.port", () -> redis.getMappedPort(6379).toString()); }

Kafka (KafkaContainer)

import org.testcontainers.containers.KafkaContainer; import org.testcontainers.utility.DockerImageName; KafkaContainer kafka = new KafkaContainer( DockerImageName.parse("confluentinc/cp-kafka:7.6.0")); @DynamicPropertySource static void configurer(DynamicPropertyRegistry registry) { registry.add("spring.kafka.bootstrap-servers", kafka::getBootstrapServers); }

Conteneur générique (GenericContainer)

Utile pour tout service Docker non couvert par une extension :

GenericContainer<?> minio = new GenericContainer<>( DockerImageName.parse("minio/minio:latest")) .withExposedPorts(9000) .withEnv("MINIO_ROOT_USER", "minioadmin") .withEnv("MINIO_ROOT_PASSWORD", "minioadmin") .withCommand("server", "/data") .waitingFor(Wait.forHttp("/minio/health/live"));

Intégration Docker Compose

Lancer un fichier docker-compose.yml entier (plusieurs services) plutôt que des conteneurs isolés. Idéal quand les services dépendent les uns des autres (ex. PostgreSQL + migrateur Flyway, ou ZooKeeper + Kafka).

Dépendance

<dependency> <groupId>org.testcontainers</groupId> <artifactId>testcontainers</artifactId> <version>1.20.4</version> <scope>test</scope> </dependency>

Note : pas de module séparé nécessaire — DockerComposeContainer fait partie du module testcontainers de base.

Utilisation basique

import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.containers.output.Slf4jLogConsumer; import org.testcontainers.utility.DockerImageName; DockerComposeContainer<?> compose = new DockerComposeContainer<>( new File("src/test/resources/docker-compose.yml")) .withExposedService("postgres_1", 5432) .withExposedService("redis_1", 6379) .withLogConsumer("postgres_1", new Slf4jLogConsumer(LOGGER));

Chaque service exposé est alors accessible via compose.getServiceHost("postgres_1", 5432) et compose.getServicePort("postgres_1", 5432).

Exemple avec docker-compose.yml

# src/test/resources/docker-compose.yml services: postgres: image: postgres:16-alpine environment: POSTGRES_DB: testdb POSTGRES_USER: test POSTGRES_PASSWORD: test healthcheck: test: ["CMD-SHELL", "pg_isready -U test"] interval: 5s timeout: 3s retries: 5 redis: image: redis:7-alpine healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 3s timeout: 2s retries: 5

DockerComposeContainer applique automatiquement les healthcheck définis dans le fichier pour déterminer quand un service est prêt. Pas besoin de WaitingForStrategy personnalisé dans la plupart des cas.

Exemple complet : tests multi-services

@SpringBootTest @Testcontainers class ServiceIntegrationTest { static DockerComposeContainer<?> compose = new DockerComposeContainer<>( new File("src/test/resources/docker-compose.yml")) .withExposedService("postgres_1", 5432) .withExposedService("redis_1", 6379); @DynamicPropertySource static void configurer(DynamicPropertyRegistry registry) { registry.add("spring.datasource.url", () -> "jdbc:postgresql://" + compose.getServiceHost("postgres_1", 5432) + ":" + compose.getServicePort("postgres_1", 5432) + "/testdb"); registry.add("spring.datasource.username", () -> "test"); registry.add("spring.datasource.password", () -> "test"); registry.add("spring.data.redis.host", () -> compose.getServiceHost("redis_1", 6379)); registry.add("spring.data.redis.port", () -> compose.getServicePort("redis_1", 6379).toString()); } @Test void fluxCompletsAvecVraiPostgresEtRedis() { // ... tests avec Postgres + Redis opérationnels } }

Piège : les noms de service dans withExposedService() correspondent aux clés du fichier docker-compose.yml, pas les noms d’image. Pour image: postgres:16-alpine sous la clé services.postgres, le nom est postgres, pas postgres_1.

Piège : par défaut, DockerComposeContainer détruit les conteneurs à la fin des tests et supprime les volumes. Pour conserver les données entre les exécutions de tests :

.withRemoveVolumes(false)

À utiliser uniquement en local (dev) — en CI, laisser le comportement par défaut pour éviter l’accumulation de volumes orphelins.

Alternative : GenericContainer + DockerCompose en one-shot

Pour un script de provisioning rapide (ex. bootstrap de base de données au premier lancement) plutôt que dans un contexte de test :

GenericContainer<?> db = new GenericContainer<>("postgres:16-alpine") .withEnv("POSTGRES_DB", "testdb") .withEnv("POSTGRES_USER", "test") .withEnv("POSTGRES_PASSWORD", "test") .withExposedPorts(5432) .waitingFor(Wait.forLogMessage(".*database system is ready to accept connections.*", 1)) .withStartupTimeout(Duration.ofMinutes(2)); db.start();

Quand choisir l’un ou l’autre ?

  • DockerComposeContainer : quand le fichier docker-compose.yml existe déjà et que vous voulez le réutiliser tel quel (cohérence dev / test / CI).
  • GenericContainer : quand la configuration est simple (un seul service) ou quand vous voulez un contrôle programmatique fin sur la stratégie d’attente.

Durée de vie des conteneurs

Statique vs non-static

DéclencheurComportement
static + @ContainerDémarrage au premier test de la classe, arrêt après le dernier test
Non-static + @ContainerDémarrage avant chaque méthode @Test, arrêt après chaque méthode
@DynamicPropertySource statiqueLancé une fois, avant le premier test de la classe

Règle : privilégier les conteneurs static sauf besoin absolu de réinitialisation par test (ex. données spécifiques par test, validation de reset state).

Parcourir plusieurs conteneurs à la suite

@Testcontainers class MultiContainerTest { @Container static PostgreSQLContainer<?> db = new PostgreSQLContainer<>("postgres:16-alpine") .withDatabaseName("testdb") .withUsername("test") .withPassword("test"); @Container static RedisContainer redis = new RedisContainer("redis:7-alpine"); @DynamicPropertySource static void configurer(DynamicPropertyRegistry registry) { registry.add("spring.datasource.url", db::getJdbcUrl); registry.add("spring.data.redis.host", redis::getHost); registry.add("spring.data.redis.port", () -> redis.getMappedPort(6379).toString()); } }

Les conteneurs partagent le même cycle de vie : tous démarrés au lancement de la classe, tous arrêtés à la fin.

Bonnes pratiques

Logs

Par défaut, les logs de Testcontainers ne s’affichent pas (pour ne pas noyer la sortie). Pour les activer :

<!-- pom.xml --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>2.0.16</version> <scope>test</scope> </dependency>

Piège : les logs de Testcontainers sont filtrés par le framework de logging JVM. Ajouter un backend de log (SLF4J Simple, Logback) dans la dépendance test pour les voir dans la console.

Ryuk (nettoyage des conteneurs orphelins)

Testcontainers utilise un conteneur Ryuk en arrière-plan pour détruire les conteneurs au cas où le processus Java serait tué (ex. crash, kill -9). Par défaut, Ryuk collecte les ressources pendant 60 secondes avant suppression.

Pour un environnement CI rapide, on peut désactiver Ryuk (à ne faire que si le processus Java s’arrête proprement) :

export TESTCONTAINERS_RYUK_DISABLED=true

Attention : sans Ryuk, les conteneurs lancés manuellement (hors @Testcontainers) ne seront jamais détruits et pollueront le daemon Docker.

@Testcontainers avec @Nested

Les conteneurs static dans une classe parente sont partagés entre toutes les classes imbriquées. Pour un conteneur isolé à une classe imbriquée :

@SpringBootTest @Testcontainers class ServiceTest { @Nested @Testcontainers class AvecPostgres { @Container static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine") .withDatabaseName("nested") .withUsername("test") .withPassword("test"); @DynamicPropertySource static void configurer(DynamicPropertyRegistry registry) { registry.add("spring.datasource.url", postgres::getJdbcUrl); // ... } } }

Vérifier la disponibilité de Docker

Testcontainers échoue silencieusement si Docker n’est pas disponible. Pour avoir une erreur claire :

@Test @DisabledIfEnvironmentVariable(named = "SKIP_DOCKER_TEST", matches = "true") void skipSiDockerPasDisponible() { // Test réel }

Intégration avec spring-boot-testcontainers

L’extension spring-boot-testcontainers (disponible depuis Spring Boot 2.4+) simplifie la configuration pour PostgreSQL et MySQL :

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-testcontainers</artifactId> <scope>test</scope> </dependency>

Avec cette extension, Spring Boot détecte automatiquement un conteneur PostgreSQL dans le contexte et injecte la configuration JDBC sans DynamicPropertySource :

@SpringBootTest @Testcontainers class ServiceTest { @Container static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine"); @Autowired JdbcTemplate jdbc; @Test void requeteSpringBootTestcontainers() { int count = jdbc.queryForObject("SELECT COUNT(*) FROM test", Integer.class); assertEquals(0, count); } }

Note : spring-boot-testcontainers automatise la configuration pour PostgreSQL et MySQL. Pour les autres services (Redis, Kafka, …), utiliser DynamicPropertySource manuellement.

Piège : spring-boot-testcontainers est disponible depuis Spring Boot 2.4, mais la détection automatique de PostgreSQLContainer ne fonctionne qu’avec Spring Boot 3.x. Avec Spring Boot 2.x, la configuration JDBC reste manuelle via DynamicPropertySource.

Résumé des pièges

PiègeImpactCorrectif
Oublier @TestcontainersConteneurs null au runtimeAjouter l’annotation au niveau de la classe de test
Conteneur non staticDémarrage/arrêt à chaque méthodeDéclarer static + @Container
Pas de backend de logLogs Testcontainers invisiblesAjouter slf4j-simple ou logback en test
Port éphémère non géréConflits entre testsUtiliser getMappedPort() à la place d’un port fixe
spring-boot-testcontainers sur Boot 2.xConfiguration JDBC manuelle nécessaireUtiliser DynamicPropertySource manuellement