Vous répondrez de tout! Contrats axés sur le consommateur à travers les yeux du développeur

Dans cet article, nous parlerons des problèmes résolus par les contrats pilotés par les consommateurs et montrerons comment l'appliquer à l'aide de l'exemple de Pacte avec Node.js et Spring Boot. Et parlez des limites de cette approche.


Problème


Lors des tests de produits, des tests de scénarios sont souvent utilisés dans lesquels l'intégration de divers composants du système dans un environnement spécialement sélectionné est vérifiée. Ces tests sur les services en direct donnent le résultat le plus fiable (sans compter les tests au combat). Mais en même temps, ils sont l'un des plus chers.

  • On pense souvent à tort que l'environnement d'intégration ne doit pas être tolérant aux pannes. SLA, les garanties pour de tels environnements sont rarement exprimées, mais s'il n'est pas disponible, les équipes doivent soit retarder les versions, soit espérer le meilleur et se battre sans tests. Bien que tout le monde sache que l' espoir n'est pas une stratégie . De plus, les nouvelles technologies d'infrastructure compliquent le travail avec les environnements d'intégration.
  • Une autre douleur est de travailler avec des données de test . De nombreux scénarios nécessitent un certain état du système, des appareils. À quel point doivent-ils être proches pour combattre les données? Comment les mettre à jour avant le test et les nettoyer après la fin?
  • Les tests sont trop instables . Et pas seulement à cause de l'infrastructure que nous avons mentionnée dans le premier paragraphe. Le test peut échouer car une équipe voisine a lancé ses propres contrôles qui ont cassé l'état attendu du système! De nombreux faux tests négatifs, tests floconneux @Ignored fin à leurs jours chez @Ignored . En outre, différentes parties de l'intégration peuvent être prises en charge par différentes équipes. Ils ont déployé une nouvelle version candidate avec des erreurs - ils ont cassé tous les consommateurs. Quelqu'un résout ce problème avec des boucles de test dédiées. Mais au prix de multiplier le coût du support.
  • De tels tests prennent beaucoup de temps . Même en pensant à l'automatisation, les résultats peuvent être attendus pendant des heures.
  • Et pour couronner le tout, si le test a vraiment bien fonctionné, il est loin d'être toujours possible de trouver immédiatement la cause du problème. Il peut se cacher profondément derrière les couches d'intégration. Ou cela peut être le résultat d'une combinaison inattendue d'états de nombreux composants du système.

Des tests stables dans un environnement d'intégration nécessitent un investissement sérieux de la part de l'AQ, des développeurs et même des opérateurs. Pas étonnant qu'ils soient tout en haut de la pyramide des tests . Ces tests sont utiles, mais l'économie des ressources ne leur permet pas de tout vérifier. La principale source de leur valeur est l'environnement.

En dessous de la même pyramide se trouvent d'autres tests dans lesquels nous échangeons la confiance pour des maux de tête de support plus petits - en utilisant des contrôles d'isolement. Plus le granulaire est petit, plus l'échelle du test est petite, moins la dépendance à l'environnement externe est importante. Tout en bas de la pyramide se trouvent des tests unitaires. Nous vérifions les fonctions individuelles, les classes, nous opérons moins avec la sémantique métier qu'avec les constructions d'une implémentation spécifique. Ces tests donnent un retour rapide.

Mais dès que nous descendons la pyramide, nous devons remplacer l'environnement par quelque chose. Les stubs apparaissent - comme des services entiers et des entités individuelles du langage de programmation. C'est à l'aide de fiches que nous pouvons tester les composants isolément. Mais ils réduisent également la validité des chèques. Comment s'assurer que le talon renvoie les données correctes? Comment garantir sa qualité?

La solution peut être une documentation complète qui décrit divers scénarios et états possibles des composants du système. Mais toute formulation laisse encore la liberté d'interprétation. Par conséquent, une bonne documentation est un artefact vivant qui s'améliore constamment à mesure que l'équipe comprend le problème. Comment alors assurer le respect des talons de documentation?

Sur de nombreux projets, vous pouvez observer une situation où les talons sont écrits par les mêmes gars qui ont développé l'artefact de test. Par exemple, les développeurs d'applications mobiles créent eux-mêmes des talons pour leurs tests. En conséquence, les programmeurs peuvent comprendre la documentation à leur manière (ce qui est tout à fait normal), ils créent le stub avec le mauvais comportement attendu, écrivent le code conformément à celui-ci (avec des tests verts) et des erreurs se produisent lors de la véritable intégration.

De plus, la documentation se déplace généralement en aval - les clients utilisent des spécifications de services (dans ce cas, un autre service peut être client du service). Il n'exprime pas comment les consommateurs utilisent les données, quelles données sont nécessaires, quelles hypothèses ils font pour ces données. La conséquence de cette ignorance est la loi d'Hyrum .



Hyrum Wright développe depuis longtemps des outils publics au sein de Google et a observé comment les plus petits changements peuvent provoquer des pannes pour les clients qui ont utilisé les fonctionnalités implicites (non documentées) de ses bibliothèques. Cette connectivité cachée complique l'évolution de l'API.

Ces problèmes peuvent être résolus dans une certaine mesure à l'aide de contrats axés sur le consommateur. Comme toute approche et tout outil, il a une gamme d'applicabilité et de coût, que nous considérerons également. Les implémentations de cette approche ont atteint un niveau de maturité suffisant pour essayer leurs projets.

Qu'est-ce qu'un CDC?


Trois éléments clés:

  • Le contrat . Décrit à l'aide de DSL, dépend de l'implémentation. Il contient une description de l'API sous forme de scénarios d'interaction: si une demande spécifique arrive, le client doit recevoir une réponse spécifique.
  • Tests clients . De plus, ils utilisent un talon, qui est généré automatiquement à partir du contrat.
  • Tests pour l'API . Ils sont également générés à partir du contrat.

Ainsi, le contrat est exécutable. Et la principale caractéristique de l'approche est que les exigences pour le comportement de l'API vont en amont , du client au serveur.

Le contrat se concentre sur le comportement qui compte vraiment pour le consommateur. Rend ses hypothèses sur l'API explicites.

L'objectif principal du CDC est d'apporter une compréhension du comportement de l'API à ses développeurs et aux développeurs de ses clients. Cette approche est bien combinée avec BDD, lors de réunions de trois amigo, vous pouvez esquisser les blancs pour le contrat. En fin de compte, ce contrat sert également à améliorer les communications; partager une compréhension commune de la problématique et mettre en œuvre la solution au sein et entre les équipes.

Pacte


Envisagez d'utiliser CDC comme exemple de Pact, l'une de ses implémentations. Supposons que nous créons une application Web pour les participants à la conférence. Dans la prochaine itération, l'équipe élabore un calendrier de présentation - jusqu'à présent sans histoires comme le vote ou les notes, uniquement la sortie de la grille des rapports. Le code source de l'exemple est ici .

Lors d'une réunion de trois quatre amigo, un produit, un testeur, les développeurs du backend et une application mobile se rencontrent. Ils disent que

  • Une liste avec le texte sera affichée dans l'interface utilisateur: titre du rapport + conférenciers + date et heure.
  • Pour ce faire, le backend doit renvoyer des données comme dans l'exemple ci-dessous.

 { "talks":[ { "title":"      ", "speakers":[ { "name":" " } ], "time":"2019-05-27T12:00:00+03:00" } ] } 

Après quoi le développeur frontend va écrire le code client (backend pour frontend). Il installe une bibliothèque de contrats de pacte dans le projet:

 yarn add --dev @pact-foundation/pact 

Et commence à écrire un test. Il configure le serveur de stub local, qui simulera le service avec des planifications de rapports:

 const provider = new Pact({ //      consumer: "schedule-consumer", provider: "schedule-producer", // ,     port: pactServerPort, //  pact     log: path.resolve(process.cwd(), "logs", "pact.log"), // ,     dir: path.resolve(process.cwd(), "pacts"), logLevel: "WARN", //  DSL  spec: 2 }); 

Le contrat est un fichier JSON qui décrit les scénarios d'interaction du client avec le service. Mais vous n'avez pas besoin de le décrire manuellement, car il est formé à partir des paramètres du stub dans le code. Le développeur avant le test décrit le comportement suivant.

 provider.setup().then(() => provider .addInteraction({ uponReceiving: "a request for schedule", withRequest: { method: "GET", path: "/schedule" }, willRespondWith: { status: 200, headers: { "Content-Type": "application/json;charset=UTF-8" }, body: { talks: [ { title: "      ", speakers: [ { name: " " } ], time: "2019-05-27T12:00:00+03:00" } ] } } }) .then(() => done()) ); 

Ici, dans l'exemple, nous avons spécifié la demande de service attendue spécifique, mais pact-js prend également en charge plusieurs méthodes pour déterminer les correspondances .

Enfin, le programmeur écrit un test de la partie du code qui utilise ce stub. Dans l'exemple suivant, nous l'appellerons directement pour plus de simplicité.

 it("fetches schedule", done => { fetch(`http://localhost:${pactServerPort}/schedule`) .then(response => response.json()) .then(json => expect(json).toStrictEqual({ talks: [ { title: "      ", speakers: [ { name: " " } ], time: "2019-05-27T12:00:00+03:00" } ] })) .then(() => done()); }); 

Dans un projet réel, cela peut être soit un test unitaire rapide d'une fonction d'interprétation de réponse distincte, soit un test d'interface utilisateur lent pour afficher les données reçues d'un service.

Pendant le test, pact vérifie que le stub a reçu la demande spécifiée dans les tests. Les écarts peuvent être considérés comme différents dans le fichier pact.log.

 E, [2019-05-21T01:01:55.810194 #78394] ERROR -- : Diff with interaction: "a request for schedule" Diff -------------------------------------- Key: - is expected + is actual Matching keys and values are not shown { "headers": { - "Accept": "application/json" + "Accept": "*/*" } } Description of differences -------------------------------------- * Expected "application/json" but got "*/*" at $.headers.Accept 


Si le test réussit, un contrat est généré au format JSON. Il décrit le comportement attendu de l'API.

 { "consumer": { "name": "schedule-consumer" }, "provider": { "name": "schedule-producer" }, "interactions": [ { "description": "a request for schedule", "request": { "method": "GET", "path": "/schedule", "headers": { "Accept": "application/json" } }, "response": { "status": 200, "headers": { "Content-Type": "application/json;charset=UTF-8" }, "body": { "talks":[ { "title":"      ", "speakers":[ { "name":" " } ], "time":"2019-05-27T12:00:00+03:00" } ] }}} ], "metadata": { "pactSpecification": { "version": "2.0.0" } } } 

Il donne ce contrat au développeur backend. Disons que l'API est sur Spring Boot. Pact possède une bibliothèque pact-jvm-provider-spring qui peut fonctionner avec MockMVC. Mais nous allons jeter un œil au Spring Cloud Contract, qui implémente CDC dans l'écosystème Spring. Il utilise son propre format de contrats, mais dispose également d'un point d'extension pour connecter des convertisseurs d'autres formats. Son format de contrat natif n'est pris en charge que par le contrat Spring Cloud lui-même - contrairement à Pact, qui possède des bibliothèques pour JVM, Ruby, JS, Go, Python, etc.

Supposons, dans notre exemple, que le développeur principal utilise Gradle pour créer le service. Il connecte les dépendances suivantes:

 buildscript { // ... dependencies { classpath "org.springframework.cloud:spring-cloud-contract-pact:2.1.1.RELEASE" } } plugins { id "org.springframework.cloud.contract" version "2.1.1.RELEASE" // ... } // ... dependencies { // ... testImplementation 'org.springframework.cloud:spring-cloud-starter-contract-verifier' } 

Et il place le contrat Pact reçu du soumissionnaire dans le répertoire src/test/resources/contracts .

Par défaut, le plugin Spring-Cloud-Contract soustrait les contrats. Pendant l'assemblage, la tâche Gradle generateContractTests est exécutée, ce qui génère le test suivant dans le répertoire build / generated-test-sources.

 public class ContractVerifierTest extends ContractsBaseTest { @Test public void validate_aggregator_client_aggregator_service() throws Exception { // given: MockMvcRequestSpecification request = given() .header("Accept", "application/json"); // when: ResponseOptions response = given().spec(request) .get("/scheduler"); // then: assertThat(response.statusCode()).isEqualTo(200); assertThat(response.header("Content-Type")).isEqualTo("application/json;charset=UTF-8"); // and: DocumentContext parsedJson = JsonPath.parse(response.getBody().asString()); assertThatJson(parsedJson).array("['talks']").array("['speakers']").contains("['name']").isEqualTo( /*...*/ ); assertThatJson(parsedJson).array("['talks']").contains("['time']").isEqualTo( /*...*/ ); assertThatJson(parsedJson).array("['talks']").contains("['title']").isEqualTo( /*...*/ ); } } 


Au démarrage de ce test, nous verrons une erreur:

 java.lang.IllegalStateException: You haven't configured a MockMVC instance. You can do this statically 

Comme nous pouvons utiliser différents outils pour les tests, nous devons indiquer au plug-in celui que nous avons configuré. Cela se fait via la classe de base, qui héritera des tests générés par les contrats.

 public abstract class ContractsBaseTest { private ScheduleController scheduleController = new ScheduleController(); @Before public void setup() { RestAssuredMockMvc.standaloneSetup(scheduleController); } } 


Pour utiliser cette classe de base lors de la génération, vous devez configurer le plugin gradle Spring-Cloud-Contract.

 contracts { baseClassForTests = 'ru.example.schedule.ContractsBaseTest' } 


Maintenant, nous avons généré le test suivant:
 public class ContractVerifierTest extends ContractsBaseTest { @Test public void validate_aggregator_client_aggregator_service() throws Exception { // ... } } 

Le test démarre correctement, mais échoue avec une erreur de vérification - le développeur n'a pas encore écrit l'implémentation du service. Mais maintenant, il peut le faire sur la base d'un contrat. Il peut s’assurer qu’il est en mesure de traiter la demande du client et de renvoyer la réponse attendue.

Le développeur de services sait par le contrat ce qu'il doit faire, quel comportement mettre en œuvre.

Le pacte peut être intégré plus profondément dans le processus de développement. Vous pouvez déployer un courtier Pact qui regroupe ces contrats, prend en charge leur versioning et peut afficher un graphique de dépendance.



Le téléchargement d'un nouveau contrat généré vers le courtier peut être effectué à l'étape CI lors de la création du client. Et dans le code du serveur, indiquez le chargement dynamique du contrat par URL. Spring Cloud Contract prend également cela en charge.

Applicabilité CDC


Quelles sont les limites des contrats axés sur le consommateur?

Pour utiliser cette approche, vous devez payer avec des outils supplémentaires comme le pacte. Les contrats en soi sont un artefact supplémentaire, une autre abstraction qui doit être soigneusement entretenue et appliquée consciemment des pratiques d'ingénierie.

Ils ne remplacent pas les tests e2e , car les stubs restent des stubs - des modèles de composants système réels, qui peuvent être un peu, mais ne correspondent pas à la réalité. Grâce à eux, les scénarios complexes ne peuvent pas être vérifiés.

De plus, les CDC ne remplacent pas les tests fonctionnels de l'API . Ils sont plus coûteux à gérer que les tests unitaires simples anciens. Les développeurs de Pact recommandent d'utiliser les heuristiques suivantes - si vous supprimez le contrat et que cela ne provoque pas d'erreurs ou d'interprétation erronée par le client, cela n'est pas nécessaire. Par exemple, il n'est pas nécessaire de décrire absolument tous les codes d'erreur API via un contrat si le client les traite de la même manière. En d'autres termes, le contrat ne décrit pour le service que ce qui est important pour son client . Pas plus, mais pas moins.

Trop de contrats compliquent également l'évolution de l'API. Chaque contrat supplémentaire est l'occasion de tests rouges . Il est nécessaire de concevoir un CDC de telle manière que chaque test d'échec porte une charge sémantique utile qui l'emporte sur le coût de son support. Par exemple, si le contrat fixe la longueur minimale d'un certain champ de texte indifférent au consommateur (il utilise la technique Toleran Reader ), chaque modification de cette valeur minimale rompra le contrat et les nerfs de ceux qui l'entourent. Une telle vérification doit être transférée au niveau de l'API elle-même et implémentée en fonction de la source des restrictions.

Conclusion


CDC améliore la qualité du produit en décrivant explicitement le comportement d'intégration. Il aide les clients et les développeurs de services à parvenir à une compréhension commune, vous permet de parler de code. Mais cela se fait au prix de l'ajout d'outils, de l'introduction de nouvelles abstractions et d'actions supplémentaires des membres de l'équipe.

Dans le même temps, les outils et frameworks CDC sont activement développés et sont déjà arrivés à maturité pour tester vos projets. Test :)

Lors de la conférence QualityConf du 27 au 28 mai, Andrei Markelov parlera des techniques de test sur prod, et Arthur Khineltsev parlera de la surveillance d'un front-end très chargé, alors que le prix d'une petite erreur est de dizaines de milliers d'utilisateurs tristes.

Venez discuter pour la qualité!

Source: https://habr.com/ru/post/fr452960/


All Articles