Testcontainers
Contexte
Les tests d'intégration nécessitent des dépendances externes (base de données, broker Kafka, cache Redis). Les approches classiques posent problème :
- mocks — ne testent pas le comportement réel
- base partagée — conflits entre développeurs, état imprévisible
- H2 en mémoire — comportement différent de PostgreSQL en production
Testcontainers résout ce problème en démarrant de vrais containers Docker pendant les tests.
Principe
Testcontainers lance des containers Docker jetables pour chaque suite de tests :
Test démarre → Container PostgreSQL créé → Tests exécutés → Container détruit
Les tests tournent contre de vraies instances, identiques à la production.
Exemple avec Spring Boot et PostgreSQL
Dépendance
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
Configuration
@SpringBootTest
@Testcontainers
class UserRepositoryTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Autowired
private UserRepository userRepository;
@Test
void shouldSaveAndFindUser() {
User user = new User("Ali", "ali@test.com");
userRepository.save(user);
Optional<User> found = userRepository.findByEmail("ali@test.com");
assertThat(found).isPresent();
assertThat(found.get().getName()).isEqualTo("Ali");
}
}
Exemple avec Kafka
@Container
static KafkaContainer kafka = new KafkaContainer(
DockerImageName.parse("confluentinc/cp-kafka:7.5.0")
);
@DynamicPropertySource
static void kafkaProperties(DynamicPropertyRegistry registry) {
registry.add("spring.kafka.bootstrap-servers", kafka::getBootstrapServers);
}
Pattern : base de test partagée
Pour accélérer les tests, on peut partager un container entre toutes les classes de test :
public abstract class AbstractIntegrationTest {
static final PostgreSQLContainer<?> postgres;
static {
postgres = new PostgreSQLContainer<>("postgres:16")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
postgres.start();
}
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
}
Les classes de test héritent de AbstractIntegrationTest — un seul container pour toute la suite.
Containers disponibles
| Module | Image | Usage |
|---|---|---|
postgresql |
postgres:16 | Base relationnelle |
kafka |
confluentinc/cp-kafka | Broker Kafka |
mongodb |
mongo:7 | Base document |
localstack |
localstack/localstack | AWS (S3, SQS, DynamoDB) |
elasticsearch |
elasticsearch:8 | Moteur de recherche |
Avantages
- tests contre des vraies dépendances, pas des mocks
- environnement identique pour chaque développeur
- pas de pollution entre les tests
- fonctionne en CI/CD (Docker-in-Docker ou Docker socket)
Inconvénients
- nécessite Docker sur la machine de développement
- plus lent que des tests unitaires purs
- consomme des ressources (CPU, RAM, disque)
À retenir
- Testcontainers remplace les mocks et H2 par de vraies instances
- utiliser
@DynamicPropertySourceavec Spring Boot pour injecter les URLs - partager les containers entre les tests pour la performance
- indispensable pour les tests d'intégration en architecture microservices