Lors de la Devoxx 2022 se déroulant au Palais des Congrès de Paris, Julien Durillon développeur, OPS et co-fondateur de Clever Cloud, nous a exposé son retour d’expérience de Testcontainers, bibliothèque Java permettant de disposer d’un conteneur Docker pour les tests.
Dans l’espace de quelques minutes, temps alloué pour la conférence, il nous a présenté Testcontainers et son utilisation dans le système de facturation chez Clever Cloud.
Ayant déjà eu l’opportunité d’utiliser Testcontainers dans un projet professionnel, je vous propose, à travers cet article, de revenir sur cette bibliothèque et de partager avec vous quelques notes.
Pourquoi ?
En ce qui concerne les tests, il est inutile de rappeler qu’ils sont indispensables pour tout projet réussi. Les tests sont devenus aujourd’hui une partie intégrante du code. En effet, l’émergence et l’adoption des méthodologies qui s’articulent autour des tests (TDD, BDD…) ont largement contribué à rendre les tests quasi incontournables pour tout code de qualité.
Parmi les objectifs à maintenir tout au long du projet, une couverture de tests satisfaisante reste de mise.
Plusieurs catégories de tests sont envisageables lors du développement : test unitaire, test d’intégration, test de charge, test d’IHM… Comme les tests n’ont pas forcément la même valeur en termes de coût, de complexité, de rapidité d’exécution, plusieurs stratégies de tests ont été observées par le passé. Les tests unitaires ont été, souvent, favorisés vu leur simplicité et leur rapidité d’exécution et se retrouvaient ainsi à la base de la pyramide de tests. Toutefois, avec le développement des micro-services, la pyramide de tests a tendance à s’équilibrer.
En effet, les interactions entre les micro-services demandent plus de tests représentatifs du système. Ainsi, les projets basés sur les micro-services ciblent d’avantage les tests fonctionnels de bout en bout proches de l’utilisateur et tendent à capitaliser sur les tests d’intégration au détriment des tests unitaires.
Comme il est souvent nécessaire de s’affranchir de la base de données, les tests d’intégrations ont été traditionnellement accompagnés par la mise en place d’une base mémoire (H2 par exemple). Toutefois, cette approche reste limitée même si elle est largement supportée par les différentes bibliothèques disponibles. En effet, une base H2 n’est pas totalement compatible, à titre d’exemple, avec Postgres.
Par ailleurs, la complexité induite par les échanges entre les micro-services via les queues et les différentes APIs complexifie encore les tests d’intégration. C’est pour répondre, entre autres, à cette complexité que Testcontainers prend sens. En effet, Testcontainers permet de disposer d’un conteneur Docker à initialiser avec toutes les dépendances requises sur lequel les tests peuvent s’appuyer.
Que permet Testcontainers ?
Testcontainers est une bibliothèque Java qui permet d’instancier un conteneur Docker lors des tests. Elle est fournie sous forme d’un JAR core et des JARs modules. Nous distinguons plusieurs options, parmi les modules disponibles :
- la création d’un conteneur d’une base de données dont l’état est contrôlé et connu d’avance. Cela nous permet de mettre en place, par exemple, des tests d’intégration complètement compatibles avec la base de données cible.
- la création d’un conteneur de service avec lequel les tests peuvent interagir
- la création d’un conteneur de serveur web embarqué (Nginx, Apache)
Nous avons également la possibilité d’utiliser un conteneur complètement customisé à base d’une image complètement personnalisée, voire de créer une image à la volée.
Testcontainers se base par défaut sur le registry Docker publique (Docker Hub) pour récupérer les images selon la stratégie Docker de gestion et de cache des images. En plus, la bibliothèque détecte automatiquement l’environnement Docker cible, vérifie la bonne configuration du système au démarrage et effectue des actions de nettoyage du système à la fin de l’exécution.
Testcontainers supporte JUnit 4 et 5. Par ailleurs, un BOM est également fourni pour simplifier la gestion des versions.
Configuration
Configuration Maven
Pour utiliser Testcontainers il suffit d’ajouter la dépendance core :
Testcontainers properties
Testcontainers supporte dans l’ordre les options de configuration suivantes :
– Variable d’environnement
– .testcontainers.properties dans le répertoire home de l’utilisateur courant
– testcontainers.properties depuis le classpath (src/test/resources)
Parmi les options de configuration possibles nous pouvons redéfinir les images Docker pour les récupérer depuis un registry privé en préfixant les images, comme suit :
Nous pouvons également accélérer légèrement les tests en désactivant la vérification effectuée à chaque lancement du conteneur :
Ou encore modifier la stratégie de détection de l’environnement Docker :
D’autres paramètres sont également disponibles et configurables, à consulter la liste complète dans la documentation officielle.
Exemple de conteneur de base de données
Instanciation implicite
Pour instancier un conteneur de base de données dans un projet Spring, il suffit de modifier l’URL de la datasource comme suit :
jdbc:postgresql:12://host:port/dbname → jdbc:tc:postgresql:12://host:port/dbname
A noter Testcontainers ne prend pas en compte la paire host:port. Ainsi la configuration de l’URL suivante est également possible (host-less URI) : jdbc:tc:postgresql:12///dbname
Avec cette modification de l’URL de la base de données et l’ajout des dépendances Testcontainers en runtime, Testcontainers instancie automatiquement un conteneur de base de données tel que configuré.
D’autres paramètres sont également configurables via l’URL, l’ajout d’un script d’initialisation par exemple est configurable comme suit :
Instanciation explicite
Pour instancier explicitement un conteneur de base de données nous pouvons utiliser le conteneur générique. Ainsi nous pouvons créer un conteneur Postgres comme suit :
new GenericContainer<>(“postgres:12”).withExposedPorts(5432)
L’utilisation du GenericContainer permet de configurer l’image avec les paramètres de base tels que le port exposé ou l’ajout d’une variable d’environnement.
Pour configurer plus finement le conteneur nous utilisons le module spécifique en question en ajoutant sa dépendance correspondante, Postgres par exemple :
Ainsi nous pouvons affiner la configuration du conteneur, par exemple, nous pouvons déclarer une base de données avec l’utilisateur associé et un script d’initialisation :
A noter que dans tous les cas Testcontainers modifie le port définit afin d’éviter la collision des ports. Ainsi, avec Spring nous devons récupérer le port exposé par Testcontainers pour pouvoir accéder au conteneur de la base. La méthode getMappedPort(_PORT_DEFINIT_) nous renvoie le port accessible à l’exécution. Ainsi, nous pouvons redéfinir l’URL de la datasource dynamiquement comme suit :
Le conteneur peut être démarré avant l’exécution de tous les tests et arrêté après, ainsi nous pouvons récupérer les url, password…
Testcontainers fournit également les annotations @Testcontainers et @Containers. En annotant la classe avec @Testcontainers, Testcontainers prend en charge l’instanciation de tous les conteneurs annotés avec @Container. Cela nous permet de nous affranchir des démarrages et des arrêts manuels du conteneur.
Conclusion
Testcontainers est une bibliothèque très utile pour lancer des tests reproductibles en complète isolation tout en étant compatible avec la base de données cible. Elle ne se limite pas à l’initialisation d’un conteneur de base de données mais elle offre la possibilité de lancer tout code conteneurisable. L’utilisation de Testcontainers s’accompagne avec un léger surcoût du temps d’exécution des tests induit par la vérification et la création des conteneurs, d’où l’intérêt de désactiver la vérification et réutiliser les conteneurs dès que possible. Il reste tout de même à noter que Testcontainers nécessite souvent des actions supplémentaires coté CI/CD pour pouvoir créer des conteneurs lors du build (Docker in Docker).
REF: https://www.testcontainers.org/
Pour lire le dernier article, cliquez ici : Développeur senior : Quelles perspectives ?