Passez à la pratique avec Elasticsearch : explorez nos notebooks d’exemples, lancez un essai gratuit sur le cloud ou testez Elastic dès maintenant sur votre machine locale.
Dans ce billet, nous allons présenter et expliquer deux façons de tester un logiciel en utilisant Elasticsearch comme dépendance d'un système externe. Nous couvrirons les tests utilisant des mocks ainsi que les tests d'intégration, nous montrerons quelques différences pratiques entre eux, et nous donnerons quelques conseils sur la façon de procéder pour chaque style.
De bons tests pour la confiance dans le système
Un bon test est un test qui augmente la confiance de chaque personne impliquée dans le processus de création et de maintenance d'un système informatique. Les tests ne sont pas censés être cool, rapides ou augmenter artificiellement la couverture du code. Les tests jouent un rôle essentiel à cet égard :
- Ce que nous voulons livrer va fonctionner en production.
- Le système répond aux exigences et aux contrats.
- Il n'y aura pas de régression à l'avenir.
- Les développeurs (et les autres membres de l'équipe concernés) sont convaincus que ce qu'ils ont créé fonctionnera.
Bien sûr, cela ne signifie pas que les tests ne peuvent pas être cool, rapides ou augmenter la couverture du code. Plus vite nous pouvons exécuter notre suite de tests, mieux c'est. C'est juste que dans la poursuite de la réduction de la durée globale de la suite de tests, nous ne devrions pas sacrifier la fiabilité, la maintenabilité et la confiance que les tests automatisés nous donnent.
De bons tests automatisés renforcent la confiance des différents membres de l'équipe :
- Les développeurs : ils peuvent confirmer que ce qu'ils font fonctionne (avant même que le code sur lequel ils travaillent ne quitte leur machine).
- L'équipe d'assurance qualité : elle a moins de tests à effectuer manuellement.
- Les opérateurs de systèmes et les SRE sont plus détendus, car les systèmes sont plus faciles à déployer et à entretenir.
Dernier point, mais non des moindres : l'architecture d'un système. Nous aimons que les systèmes soient organisés, faciles à entretenir, et que l'architecture soit propre et utile. Cependant, il arrive parfois que l'architecture sacrifie trop à l'excuse connue sous le nom de ": elle est plus testable de cette façon". Il n'y a rien de mal à être très testable, mais lorsque le système est écrit principalement pour être testable au lieu de répondre aux besoins qui justifient son existence, on se retrouve dans une situation où c'est la queue qui l'emporte.
Deux types de tests : Mocks & dépendances
Les tests peuvent être perçus et donc classés de différentes manières. Dans ce billet, je me concentrerai sur un seul aspect de la division des tests : l'utilisation de mocks (ou stubs, ou fakes, ou ...) par rapport à l'utilisation de vraies dépendances. Dans notre cas, la dépendance est Elasticsearch.
Les tests utilisant des mocks sont très rapides car ils n'ont pas besoin de démarrer des dépendances externes et tout se passe uniquement en mémoire. Dans le cadre des tests automatisés, on utilise de faux objets au lieu de vrais objets pour tester des parties d'un programme sans utiliser les dépendances réelles. C'est la raison pour laquelle ils sont nécessaires et qu'ils brillent dans tous les tests de réseaux de détection rapide, par exemple. la validation des données. Il n'est pas nécessaire de lancer une base de données et de l'appeler uniquement pour vérifier que les nombres négatifs dans une demande ne sont pas autorisés, par exemple.
Cependant, l'introduction des mocks a plusieurs implications :
- Il n'est pas possible de simuler facilement tout et tout le temps, et les simulations ont donc un impact sur l'architecture du système (ce qui est parfois très bien, parfois moins bien).
- Les tests fonctionnant sur des mocks peuvent être rapides, mais leur développement peut prendre un certain temps, car les mocks reflétant profondément les systèmes qu'ils imitent ne sont généralement pas fournis gratuitement. Quelqu'un qui sait comment le système fonctionne doit écrire les mocks de la bonne manière, et cette connaissance peut provenir d'une expérience pratique, de l'étude de la documentation, etc.
- Les objets fictifs doivent être entretenus. Lorsque votre système dépend d'une dépendance externe et que vous devez mettre à jour cette dépendance, quelqu'un doit s'assurer que les mocks imitant la dépendance sont également mis à jour avec tous les changements : cassures, documentés et non documentés (qui peuvent également avoir un impact sur notre système). Cela devient particulièrement pénible lorsque vous voulez mettre à jour une dépendance mais que votre suite de tests (qui n'utilise que des mocks) ne peut pas vous donner l'assurance que tous les cas testés sont garantis de fonctionner.
- Il faut de la discipline pour s'assurer que les efforts sont consacrés au développement et aux tests du système, et non aux simulacres.
Pour ces raisons, de nombreuses personnes préconisent d'aller exactement dans la direction opposée : ne jamais utiliser de mocks (ou de stubs, etc.), mais s'appuyer uniquement sur les dépendances réelles. Cette approche fonctionne très bien dans les démonstrations ou lorsque le système est minuscule et ne comporte que quelques cas de test générant une couverture importante. Ces tests peuvent être des tests d'intégration (grosso modo : vérification d'une partie d'un système par rapport à certaines dépendances réelles) ou des tests de bout en bout (utilisation simultanée de toutes les dépendances réelles et vérification du comportement du système à toutes les extrémités, tout en jouant les flux de travail de l'utilisateur qui définissent le système comme utilisable et performant). Un avantage évident de cette approche est que nous vérifions également (souvent involontairement) nos hypothèses sur les dépendances et la manière dont nous les intégrons dans le système sur lequel nous travaillons.
Cependant, lorsque les tests n'utilisent que des dépendances réelles, nous devons prendre en compte les aspects suivants :
- Certains scénarios de test n'ont pas besoin de la dépendance réelle (par exemple, pour vérifier les invariants statiques d'une demande).
- Ces tests ne sont généralement pas exécutés par séries entières sur les machines des développeurs, car l'attente d'un retour d'information prendrait trop de temps.
- Ils nécessitent plus de ressources sur les machines d'IC, et il faut parfois plus de temps pour régler les choses afin de ne pas perdre de temps sur &.
- Il n'est pas toujours facile d'initialiser les dépendances avec des données de test.
- Les tests avec des dépendances réelles sont parfaits pour isoler le code avant une refonte majeure, une migration ou une mise à niveau des dépendances.
- Il est plus probable qu'il s'agisse de tests opaques, c'est-à-dire qu'ils ne détaillent pas les aspects internes du système testé, mais s'intéressent à leurs résultats.
Le bon choix : utiliser les deux tests
Au lieu de tester votre système avec un seul type de test, vous pouvez vous appuyer sur les deux types de test lorsque c'est utile et essayer d'améliorer votre utilisation des deux.
- Exécutez d'abord les tests basés sur des simulacres parce qu'ils sont beaucoup plus rapides, et seulement lorsque tous les tests sont réussis, exécutez les tests de dépendance plus lents seulement après.
- Choisissez les mocks pour les scénarios où les dépendances externes ne sont pas vraiment nécessaires : lorsque le mocking prendrait trop de temps, le code devrait être massivement modifié juste pour cela ; comptez sur les dépendances externes.
- Il n'y a rien de mal à tester un morceau de code en utilisant les deux approches, tant que cela a un sens.
Exemple de SystemUnderTest
Pour les sections suivantes, nous allons utiliser un exemple qui se trouve ici. Il s'agit d'une petite application de démonstration écrite en Java 21, utilisant Maven comme outil de construction, s'appuyant sur le client Elasticsearch et utilisant le dernier ajout d'Elasticsearch, utilisant ES|QL (le nouveau langage de requête procédural d'Elastic). Si Java n'est pas votre langage de programmation, vous devriez tout de même être en mesure de comprendre les concepts que nous allons aborder ci-dessous et de les transposer dans votre pile. C'est juste que l'utilisation d'un exemple de code réel rend certaines choses plus faciles à expliquer.
Le site BookSearcher nous aide à gérer la recherche et l'analyse de données, à savoir des livres dans notre cas (comme nous l'avons démontré dans l 'un des articles précédents).
- Il nécessite Elasticsearch exactement dans la version
8.15.xcomme seule dépendance (voirisCompatibleWithBackend()), par exemple parce que nous ne sommes pas sûrs que notre code soit compatible avec le futur, et nous sommes sûrs qu'il n'est pas compatible avec le passé. Avant de mettre à niveau Elasticsearch en production vers une version plus récente, nous devons d'abord le tester pour nous assurer que le comportement du système testé reste le même. - Nous pouvons l'utiliser pour rechercher le nombre de livres publiés au cours d'une année donnée (voir
numberOfBooksPublishedInYear). - Nous pouvons également l'utiliser lorsque nous devons analyser notre ensemble de données et trouver les 20 auteurs les plus publiés entre deux années données (voir
mostPublishedAuthorsInYears).
Tester avec des mocks pour commencer
Pour créer les mocks utilisés dans nos tests, nous allons utiliser Mockito, une bibliothèque de mocking très populaire dans l'écosystème Java.
Nous pourrions commencer par ce qui suit, pour que les simulations soient réinitialisées avant chaque test :
Comme nous l'avons dit précédemment, tout ne peut pas être facilement testé à l'aide de mocks. Mais il y a des choses que l'on peut (et que l'on doit probablement) faire. Essayons de vérifier que la seule version supportée d'Elasticsearch est 8.15.x pour l'instant (à l'avenir, nous pourrions étendre la gamme une fois que nous aurons confirmé que notre système est compatible avec les versions futures) :
Nous pouvons vérifier de la même manière (simplement en renvoyant une version mineure différente) que notre site BookSearcher ne fonctionnera pas encore avec 8.16.x, car nous ne sommes pas sûrs qu'il sera compatible avec lui :
Voyons maintenant comment nous pouvons obtenir quelque chose de similaire en testant contre un vrai Elasticsearch. Pour cela, nous allons utiliser le module Elasticsearch de Testcontainers, qui n'a qu'une seule exigence : il a besoin d'un accès à Docker, car il exécute des conteneurs Docker pour vous. D'un certain point de vue, Testcontainers est simplement un moyen d'exploiter les conteneurs Docker, mais au lieu de le faire dans votre Docker Desktop (ou similaire), dans votre CLI, ou dans des scripts, vous pouvez exprimer vos besoins dans le langage de programmation que vous connaissez. Cela permet de récupérer des images, de démarrer des conteneurs, de les ramasser après les tests, de copier des fichiers dans les deux sens, d'exécuter des commandes, d'examiner les journaux, etc. directement à partir de votre code de test.
Le talon pourrait ressembler à ceci :
Dans cet exemple, nous nous appuyons sur l'intégration JUnit de Testcontainers avec @Testcontainers et @Container, ce qui signifie que nous n'avons pas à nous soucier de démarrer Elasticsearch avant nos tests et de l'arrêter après. La seule chose à faire est de créer le client avant chaque test et de le fermer après chaque test (pour éviter les fuites de ressources, qui pourraient avoir un impact sur des suites de tests plus importantes).
Annoter un champ non statique avec @Container signifie qu'un nouveau conteneur sera démarré pour chaque test, nous n'avons donc pas à nous soucier des données périmées ou de la réinitialisation de l'état du conteneur. Cependant, avec de nombreux tests, cette approche pourrait ne pas donner de bons résultats, c'est pourquoi nous allons la comparer à d'autres solutions dans l'un des prochains articles.
Remarque :
En vous appuyant surdocker.elastic.co(le dépôt d'images Docker officiel d'Elastic), vous évitez d'épuiser vos limites sur le hub Docker.
Il est également recommandé d'utiliser la même version de votre dépendance dans votre environnement de test et de production, afin de garantir une compatibilité maximale. Nous recommandons également d'être précis dans la sélection de la version, pour cette raison, il n'y a pas de baliselatestpour les images Elasticsearch.
Connexion à Elasticsearch dans les tests
Le client Java Elasticsearch est capable de se connecter à Elasticsearch fonctionnant dans un conteneur de test même si la sécurité et SSL/TLS sont activés (ce qui est le cas par défaut pour les versions 8.x, c'est pourquoi nous n'avons pas eu à spécifier quoi que ce soit relatif à la sécurité dans la déclaration du conteneur). En supposant que l'Elasticsearch que vous utilisez en production dispose également de TLS et d'une certaine sécurité, il est recommandé d'opter pour une configuration de test d'intégration aussi proche que possible du scénario de production, et donc de ne pas les désactiver dans les tests.
Comment obtenir les données nécessaires à la connexion, en supposant que le conteneur soit affecté au champ ou à la variable elasticsearch:
elasticsearch.getHost()vous donnera l'hôte sur lequel le conteneur fonctionne (qui la plupart du temps sera probablement"localhost", mais ne le codifiez pas en dur car parfois, en fonction de votre configuration, il peut s'agir d'un autre nom, donc l'hôte doit toujours être obtenu dynamiquement).elasticsearch.getMappedPort(9200)donnera le port hôte que vous devez utiliser pour vous connecter à Elasticsearch à l'intérieur du conteneur (parce qu'à chaque fois que vous démarrez le conteneur, le port extérieur est différent, donc cela doit être un appel dynamique également).- Sauf s'ils ont été écrasés, le nom d'utilisateur et le mot de passe par défaut sont respectivement
"elastic"et"changeme". - Si aucun certificat SSL/TLS n'a été spécifié lors de la configuration du conteneur et que la connectivité sécurisée n'est pas désactivée (ce qui est le comportement par défaut à partir des versions 8.x), un certificat auto-signé est généré. Pour lui faire confiance (par exemple comme le fait cURL), le certificat peut être obtenu en utilisant
elasticsearch.caCertAsBytes()(qui renvoieOptional<byte[]>), ou une autre méthode pratique consiste à obtenirSSLContexten utilisantcreateSslContextFromCa().
Le résultat global pourrait ressembler à ceci :
Un autre exemple de création d'une instance de ElasticsearchClient se trouve dans le projet de démonstration.
Remarque:
Pour la création de clients dans des environnements de production, veuillez vous référer à la documentation.
Premier test d'intégration
Notre tout premier test, qui consiste à vérifier que nous pouvons créer BookSearcher à l'aide de la version 8.15.x d'Elasticsearch, pourrait ressembler à ceci :
Comme vous pouvez le constater, nous n'avons besoin de rien d'autre. Nous n'avons pas besoin de simuler la version renvoyée par Elasticsearch, la seule chose que nous devons faire est de fournir à BookSearcher un client connecté à une instance réelle d'Elasticsearch, qui a été démarrée pour nous par Testcontainers.
Les tests d'intégration se soucient moins des aspects internes
Faisons une petite expérience : supposons que nous devions cesser d'extraire des données de l'ensemble de résultats à l'aide d'indices de colonnes, mais que nous devions nous appuyer sur les noms de colonnes. Ainsi, dans la méthode isCompatibleWithBackend, au lieu de
que nous allons avoir :
Lorsque nous réexécutons les deux tests, nous remarquons que le test d'intégration avec le vrai Elasticsearch passe toujours sans problème. Cependant, les tests utilisant des simulacres ont cessé de fonctionner, parce que nous avons simulé des appels tels que rs.getInt(int), et non rs.getInt(String). Pour les faire passer, nous devons maintenant soit les simuler à la place, soit les simuler tous les deux, en fonction des autres cas d'utilisation que nous avons dans notre suite de tests.
Les tests d'intégration peuvent être un canon pour tuer une mouche
Les tests d'intégration permettent de vérifier le comportement du système, même si des dépendances externes ne sont pas nécessaires. Cependant, cette utilisation est généralement une perte de temps et de ressources d'exécution. Examinons la méthode mostPublishedAuthorsInYears(int minYear, int maxYear). Les deux premières lignes sont les suivantes :
La première instruction vérifie une condition qui ne dépend pas d'Elasticsearch (ou de toute autre dépendance externe) de quelque manière que ce soit. Par conséquent, il n'est pas nécessaire de lancer des conteneurs pour vérifier simplement que si minYear est supérieur à maxYear, une exception est levée.
Un simple test de simulation, qui est également rapide et peu gourmand en ressources, est plus que suffisant pour s'en assurer. Après avoir mis en place les simulacres, nous pouvons simplement opter pour :
Lancer une dépendance, au lieu de faire du mocking, serait un gaspillage dans ce cas de test car il n'y a aucune chance de faire un appel significatif pour cette dépendance.
Cependant, pour vérifier le comportement à partir de String query = ..., que la requête est écrite correctement, les résultats sont conformes aux attentes : la bibliothèque du client est capable d'envoyer des requêtes et des réponses correctes, il n'y a pas de changement de syntaxe et il est donc beaucoup plus facile d'utiliser un test d'intégration, par exemple :
De cette façon, nous pouvons être assurés que lorsque nous envoyons nos données à Elasticsearch (dans cette version ou dans toute version future vers laquelle nous choisirons de migrer), notre requête nous donnera exactement ce que nous attendions : le format des données n'a pas changé, la requête est toujours valide, et tous les middleware (clients, pilotes, sécurité, etc.) continueront à fonctionner. Nous n'avons pas à nous préoccuper de maintenir les mocks à jour, le seul changement nécessaire pour assurer la compatibilité avec, par exemple, le système de gestion de l'information de l'entreprise. 8.15 changerait cela :
Il en va de même si vous décidez par exemple de utiliser le bon vieux QueryDSL au lieu de ES|QL : les résultats que vous recevez de la requête (quelle que soit la langue) devraient toujours être les mêmes.
Utiliser les deux approches si nécessaire
Le cas de la méthode mostPublishedAuthorsInYears illustre le fait qu'une seule méthode peut être testée à l'aide des deux méthodes. Et peut-être même qu'il devrait l'être.
- Le fait de n'utiliser que des maquettes signifie que nous devons maintenir la maquette et que nous n'avons aucune confiance dans la mise à jour de notre système.
- Le fait de n'utiliser que des tests d'intégration signifierait que nous gaspillons beaucoup de ressources sans en avoir besoin.
Récapitulons
- Il est possible d'utiliser des tests d'intégration et de simulation avec Elasticsearch.
- Utiliser des tests de simulation comme filet de détection rapide et seulement s'ils passent avec succès, lancer des tests avec des dépendances (par exemple en utilisant
./mvnw test '-Dtest=!TestInt*' && ./mvnw test '-Dtest=TestInt*'ou les plugins Failsafe et Surefire ). - Utilisez des mocks pour tester le comportement de votre système ("lignes de code") lorsque l'intégration avec des dépendances externes n'a pas vraiment d'importance (ou pourrait même être ignorée).
- Utilisez des tests d'intégration pour vérifier vos hypothèses sur les systèmes externes et leur intégration.
- N'hésitez pas à tester les deux approches - si cela se justifie - en fonction des points ci-dessus.
On pourrait faire remarquer que le fait d'être aussi strict en ce qui concerne la version (dans notre cas 8.15.x) est excessif. L'utilisation de la seule étiquette de la version pourrait l'être, mais sachez que dans ce billet, elle sert de représentation de toutes les autres caractéristiques susceptibles de changer entre les versions.
Dans le prochain article de la série, nous verrons comment initialiser Elasticsearch dans un conteneur de test, avec des ensembles de données de test. Faites-nous savoir si vous avez construit quelque chose à partir de ce blog ou si vous avez des questions sur nos forums de discussion et le canal Slack de la communauté.

