paint-brush
Création de tests d'intégration efficaces : meilleures pratiques et outils dans le cadre Springpar@avvero
531 lectures
531 lectures

Création de tests d'intégration efficaces : meilleures pratiques et outils dans le cadre Spring

par Anton Belyaev8m2024/05/26
Read on Terminal Reader

Trop long; Pour lire

Cet article propose des recommandations pratiques pour rédiger des tests d'intégration, démontrant comment se concentrer sur les spécifications des interactions avec les services externes, rendant les tests plus lisibles et plus faciles à maintenir. Cette approche améliore non seulement l'efficacité des tests, mais favorise également une meilleure compréhension des processus d'intégration au sein de l'application. À travers des exemples spécifiques, diverses stratégies et outils - tels que les wrappers DSL, JsonAssert et Pact - seront explorés, offrant au lecteur un guide complet pour améliorer la qualité et la visibilité des tests d'intégration.
featured image - Création de tests d'intégration efficaces : meilleures pratiques et outils dans le cadre Spring
Anton Belyaev HackerNoon profile picture
0-item

Dans le développement de logiciels modernes, des tests efficaces jouent un rôle clé pour garantir la fiabilité et la stabilité des applications.


Cet article propose des recommandations pratiques pour rédiger des tests d'intégration, démontrant comment se concentrer sur les spécifications des interactions avec les services externes, rendant les tests plus lisibles et plus faciles à maintenir. Cette approche améliore non seulement l'efficacité des tests, mais favorise également une meilleure compréhension des processus d'intégration au sein de l'application. À travers des exemples spécifiques, diverses stratégies et outils - tels que les wrappers DSL, JsonAssert et Pact - seront explorés, offrant au lecteur un guide complet pour améliorer la qualité et la visibilité des tests d'intégration.


L'article présente des exemples de tests d'intégration effectués à l'aide du Spock Framework dans Groovy pour tester les interactions HTTP dans les applications Spring. Dans le même temps, les principales techniques et approches proposées peuvent être appliquées efficacement à divers types d’interactions au-delà de HTTP.

Description du problème

L'article Writing Effective Integration Tests in Spring: Organized Testing Strategies for HTTP Request Mocking décrit une approche d'écriture de tests avec une séparation claire en étapes distinctes, chacune remplissant son rôle spécifique. Décrivons un exemple de test selon ces recommandations, mais en se moquant non pas d'une mais de deux requêtes. L'étape Act (Exécution) sera omise par souci de concision (un exemple de test complet peut être trouvé dans le référentiel du projet ).

Le code présenté est conditionnellement divisé en parties : « Code de support » (coloré en gris) et « Spécification des interactions externes » (coloré en bleu). Le code de support comprend des mécanismes et des utilitaires de test, notamment l'interception des requêtes et l'émulation des réponses. La spécification des interactions externes décrit des données spécifiques sur les services externes avec lesquels le système doit interagir pendant le test, y compris les demandes et réponses attendues. Le code de support pose les bases des tests, tandis que la spécification se rapporte directement à la logique métier et aux principales fonctions du système que nous essayons de tester.


La Spécification occupe une partie mineure du code mais représente une valeur significative pour la compréhension du test, tandis que le Code Support, occupant une plus grande partie, présente moins de valeur et est répétitif pour chaque déclaration fictive. Le code est destiné à être utilisé avec MockRestServiceServer. En se référant à l' exemple sur WireMock , on peut voir le même modèle : la spécification est presque identique et le code de support varie.


L'objectif de cet article est de proposer des recommandations pratiques pour rédiger des tests de manière à ce que l'accent soit mis sur la spécification et que le code de support passe au second plan.

Scénario de démonstration

Pour notre scénario de test, je propose un hypothétique bot Telegram qui transmet les requêtes à l'API OpenAI et renvoie les réponses aux utilisateurs.

Les contrats d'interaction avec les services sont décrits de manière simplifiée pour mettre en évidence la logique principale de l'opération. Vous trouverez ci-dessous un diagramme de séquence illustrant l'architecture de l'application. Je comprends que la conception peut soulever des questions du point de vue de l'architecture des systèmes, mais veuillez aborder cela avec compréhension : l'objectif principal ici est de démontrer une approche visant à améliorer la visibilité dans les tests.

Proposition

Cet article traite des recommandations pratiques suivantes pour la rédaction de tests :

  • Utilisation de wrappers DSL pour travailler avec des simulations.
  • Utilisation de JsonAssert pour la vérification des résultats.
  • Stockage des spécifications des interactions externes dans des fichiers JSON.
  • Utilisation des fichiers Pacte.

Utiliser des wrappers DSL pour se moquer

L'utilisation d'un wrapper DSL permet de masquer le code fictif passe-partout et fournit une interface simple pour travailler avec la spécification. Il est important de souligner que ce qui est proposé n'est pas un DSL spécifique mais une approche générale qu'il met en œuvre. Un exemple de test corrigé utilisant DSL est présenté ci-dessous ( texte complet du test ).

 setup: def openaiRequestCaptor = restExpectation.openai.completions(withSuccess("{...}")) def telegramRequestCaptor = restExpectation.telegram.sendMessage(withSuccess("{}")) when: ... then: openaiRequestCaptor.times == 1 telegramRequestCaptor.times == 1

Où la méthode restExpectation.openai.completions , par exemple, est décrite comme suit :

 public interface OpenaiMock { /** * This method configures the mock request to the following URL: {@code https://api.openai.com/v1/chat/completions} */ RequestCaptor completions(DefaultResponseCreator responseCreator); }

Avoir un commentaire sur la méthode permet, au survol du nom de la méthode dans l'éditeur de code, d'obtenir de l'aide, notamment de voir l'URL qui sera moquée.

Dans l'implémentation proposée, la déclaration de la réponse du mock est effectuée à l'aide d'instances ResponseCreator , autorisant des instances personnalisées, telles que :

 public static ResponseCreator withResourceAccessException() { return (request) -> { throw new ResourceAccessException("Error"); }; }

Un exemple de test pour des scénarios infructueux spécifiant un ensemble de réponses est présenté ci-dessous :

 import static org.springframework.http.HttpStatus.FORBIDDEN setup: def openaiRequestCaptor = restExpectation.openai.completions(openaiResponse) def telegramRequestCaptor = restExpectation.telegram.sendMessage(withSuccess("{}")) when: ... then: openaiRequestCaptor.times == 1 telegramRequestCaptor.times == 0 where: openaiResponse | _ withResourceAccessException() | _ withStatus(FORBIDDEN) | _

Pour WireMock, tout est pareil, sauf que la formation de la réponse est légèrement différente ( code de test , code de classe d'usine de réponse ).

Utilisation de l'annotation @Language("JSON") pour une meilleure intégration de l'EDI

Lors de l'implémentation d'un DSL, il est possible d'annoter les paramètres de méthode avec @Language("JSON") pour activer la prise en charge des fonctionnalités de langage pour des extraits de code spécifiques dans IntelliJ IDEA. Avec JSON, par exemple, l'éditeur traitera le paramètre de chaîne comme du code JSON, permettant ainsi des fonctionnalités telles que la coloration syntaxique, la saisie semi-automatique, la vérification des erreurs, la navigation et la recherche de structure. Voici un exemple d'utilisation de l'annotation :

 public static DefaultResponseCreator withSuccess(@Language("JSON") String body) { return MockRestResponseCreators.withSuccess(body, APPLICATION_JSON); }

Voici à quoi cela ressemble dans l'éditeur :

Utilisation de JsonAssert pour la vérification des résultats

La bibliothèque JSONAssert est conçue pour simplifier les tests des structures JSON. Il permet aux développeurs de comparer facilement les chaînes JSON attendues et réelles avec un haut degré de flexibilité, prenant en charge divers modes de comparaison.

Cela permet de passer d'une description de vérification comme celle-ci

 openaiRequestCaptor.body.model == "gpt-3.5-turbo" openaiRequestCaptor.body.messages.size() == 1 openaiRequestCaptor.body.messages[0].role == "user" openaiRequestCaptor.body.messages[0].content == "Hello!"

à quelque chose comme ça

 assertEquals("""{ "model": "gpt-3.5-turbo", "messages": [{ "role": "user", "content": "Hello!" }] }""", openaiRequestCaptor.bodyString, false)

À mon avis, le principal avantage de la deuxième approche est qu'elle garantit la cohérence de la représentation des données dans différents contextes : dans la documentation, les journaux et les tests. Cela simplifie considérablement le processus de test, offrant une flexibilité de comparaison et une précision dans le diagnostic des erreurs. Ainsi, nous gagnons non seulement du temps sur la rédaction et la maintenance des tests, mais améliorons également leur lisibilité et leur contenu informatif.

Lorsque vous travaillez dans Spring Boot, à partir d'au moins la version 2, aucune dépendance supplémentaire n'est nécessaire pour travailler avec la bibliothèque, car org.springframework.boot:spring-boot-starter-test inclut déjà une dépendance sur org.skyscreamer:jsonassert .

Stockage de la spécification des interactions externes dans des fichiers JSON

Une observation que nous pouvons faire est que les chaînes JSON occupent une part importante du test. Faut-il les cacher ? Oui et non. Il est important de comprendre ce qui apporte le plus d'avantages. Les masquer rend les tests plus compacts et simplifie la compréhension de l’essence du test au premier coup d’œil. D’un autre côté, pour une analyse approfondie, une partie des informations cruciales sur la spécification de l’interaction externe sera masquée, ce qui nécessitera des sauts supplémentaires entre les fichiers. La décision dépend de la commodité : faites ce qui vous convient le mieux.

Si vous choisissez de stocker les chaînes JSON dans des fichiers, une option simple consiste à conserver les réponses et les demandes séparément dans des fichiers JSON. Vous trouverez ci-dessous un code de test ( version complète ) démontrant une option d'implémentation :

 setup: def openaiRequestCaptor = restExpectation.openai.completions(withSuccess(fromFile("json/openai/response.json"))) def telegramRequestCaptor = restExpectation.telegram.sendMessage(withSuccess("{}")) when: ... then: openaiRequestCaptor.times == 1 telegramRequestCaptor.times == 1

La méthode fromFile lit simplement une chaîne à partir d'un fichier dans le répertoire src/test/resources et ne véhicule aucune idée révolutionnaire mais est toujours disponible dans le référentiel du projet pour référence.

Pour la partie variable de la chaîne, il est suggéré d'utiliser la substitution avec org.apache.commons.text.StringSubstitutor et de transmettre un ensemble de valeurs lors de la description du mock, par exemple :

 setup: def openaiRequestCaptor = restExpectation.openai.completions(withSuccess(fromFile("json/openai/response.json", [content: "Hello! How can I assist you today?"])))

Où la partie avec substitution dans le fichier JSON ressemble à ceci :

 ... "message": { "role": "assistant", "content": "${content:-Hello there, how may I assist you today?}" }, ...

Le seul défi pour les développeurs lorsqu'ils adoptent l'approche de stockage de fichiers est de développer un schéma de placement de fichiers approprié dans les ressources de test et un schéma de dénomination. Il est facile de commettre une erreur qui peut détériorer l'expérience de travail avec ces fichiers. Une solution à ce problème pourrait consister à utiliser des spécifications, telles que celles de Pact, dont nous parlerons plus tard.

Lorsque vous utilisez l'approche décrite dans des tests écrits en Groovy, vous pourriez rencontrer des inconvénients : IntelliJ IDEA ne prend pas en charge la navigation vers le fichier à partir du code, mais la prise en charge de cette fonctionnalité devrait être ajoutée à l'avenir . Dans les tests écrits en Java, cela fonctionne très bien.

Utiliser les fichiers de contrats du Pacte

Commençons par la terminologie.


Les tests de contrat sont une méthode de test des points d'intégration dans laquelle chaque application est testée isolément pour confirmer que les messages qu'elle envoie ou reçoit sont conformes à une entente mutuelle documentée dans un « contrat ». Cette approche garantit que les interactions entre les différentes parties du système répondent aux attentes.


Un contrat dans le contexte des tests contractuels est un document ou une spécification qui enregistre un accord sur le format et la structure des messages (demandes et réponses) échangés entre les applications. Il sert de base pour vérifier que chaque application peut traiter correctement les données envoyées et reçues par d'autres dans l'intégration.


Le contrat est établi entre un consommateur (par exemple, un client souhaitant récupérer certaines données) et un fournisseur (par exemple, une API sur un serveur fournissant les données nécessaires au client).


Les tests axés sur le consommateur sont une approche des tests de contrats dans laquelle les consommateurs génèrent des contrats lors de leurs exécutions de tests automatisés. Ces contrats sont transmis au fournisseur, qui exécute ensuite son ensemble de tests automatisés. Chaque demande contenue dans le dossier contractuel est adressée au prestataire, et la réponse reçue est comparée à la réponse attendue précisée dans le dossier contractuel. Si les deux réponses correspondent, cela signifie que le consommateur et le fournisseur de services sont compatibles.


Enfin, Pacte. Pact est un outil qui met en œuvre les idées de tests de contrats axés sur le consommateur. Il prend en charge les tests d'intégrations HTTP et d'intégrations basées sur des messages, en se concentrant sur le développement de tests axés sur le code.

Comme je l'ai mentionné plus tôt, nous pouvons utiliser les spécifications contractuelles et les outils de Pact pour notre tâche. L'implémentation pourrait ressembler à ceci ( code de test complet ) :

 setup: def openaiRequestCaptor = restExpectation.openai.completions(fromContract("openai/SuccessfulCompletion-Hello.json")) def telegramRequestCaptor = restExpectation.telegram.sendMessage(withSuccess("{}")) when: ... then: openaiRequestCaptor.times == 1 telegramRequestCaptor.times == 1

Le dossier du contrat est disponible pour examen .

L'avantage de l'utilisation de fichiers de contrat est qu'ils contiennent non seulement le corps de la demande et de la réponse, mais également d'autres éléments de la spécification des interactions externes : le chemin de la demande, les en-têtes et l'état de la réponse HTTP, ce qui permet de décrire entièrement une simulation sur la base d'un tel contrat.

Il est important de noter que, dans ce cas, nous nous limitons aux tests contractuels et ne nous étendons pas aux tests pilotés par les consommateurs. Cependant, quelqu’un voudra peut-être approfondir Pact.

Conclusion

Cet article passe en revue les recommandations pratiques pour améliorer la visibilité et l'efficacité des tests d'intégration dans le contexte du développement avec Spring Framework. Mon objectif était de me concentrer sur l'importance de définir clairement les spécifications des interactions externes et de minimiser le code passe-partout. Pour atteindre cet objectif, j'ai suggéré d'utiliser des wrappers DSL et JsonAssert, de stocker les spécifications dans des fichiers JSON et de travailler avec des contrats via Pact. Les approches décrites dans l'article visent à simplifier le processus d'écriture et de maintenance des tests, à améliorer leur lisibilité et, surtout, à améliorer la qualité des tests eux-mêmes en reflétant avec précision les interactions entre les composants du système.


Lien vers le référentiel du projet démontrant les tests - sandbox/bot .


Merci de l'attention que vous portez à cet article et bonne chance dans votre quête de rédaction de tests efficaces et visibles !