Retour d’expérience : tests fonctionnels avec Cypress

Dans le cadre d'un de nos projets de développement spécifique, l’équipe Makina Corpus a été amenée à coder un important logiciel métier avec de nombreuses entités et règles de gestion : l’Hydroportail.

Le blog Makina-corpus

Dans le cadre d'un de nos projets de développement spécifique, l’équipe Makina Corpus a été amenée à coder un important logiciel métier avec de nombreuses entités et règles de gestion : l’Hydroportail.

Une batterie d’outils existe permettant le développement de tests fonctionnels pour un projet Web. La plupart des outils testés ou utilisés jusqu’alors ne nous ont pas convaincus principalement du fait de leur prise en main et mise en place relativement ardue.
Avec ceci en tête et l’esprit ouvert, une nouvelle solution a été recherchée qui nous a finalement été proposée par une collègue aguerrie en développement JavaScript : Cypress. L’occasion de découvrir une nouvelle approche avec un soutien technique disponible à portée de main.

Consulter nos réalisations : Applicatif Hydroportail et Statistiques et cartes de l’Hydroportail.

Difficultés liées à la nature du projet

Globalement, l’Hydroportail est une application PHP/Symfony avec des écrans essentiellement gérés côté serveur (accompagné de JavaScript pour dynamiser les pages). Cette solution repose sur divers composants séparés, dont une API client en cours de développement actif par leurs équipes, dont voici quelques spécificités importantes et les problématiques associées dans le cadre des tests :

  • La quasi intégralité des données métier étant stockée dans cette API client, l’Hydroportail ne sert que d’interface de gestion au-dessus de ce composant.
  • Elle expose un grand nombre d’appels (plus d’une centaine) dont les formats de retour sont parfois complexes et souvent verbeux (protocol SANDRE au format SOAP).
  • Elle nous était livrée régulièrement sous la forme d’une machine virtuelle de taille relativement importante (environ 160 Go) au fil des développements, avec à chaque fois un jeu de données actualisé et des fonctionnalités nouvelles et/ou enrichies. La plupart des mises à jour ont nécessité des ajustements de notre applicatif et des tests associés. Si un « Mock » avait été implémenté, il aurait aussi fallu le maintenir à jour à chaque changement. D’autre part, la taille conséquente de la base de données de cette API client ne nous a pas permis de mettre en place un environnement de test qui puisse être remis à un état initial connu au début de chaque démarrage des séries de tests.
  • Elle n’a pas été conçue pour permettre de suppression de données, tout étant historisé « à jamais ».

Implémenter un « Mock » complet de cette API client serait trop coûteux. De ce fait, ce n’est pas l’approche qui a été retenue. Il a plutôt été décidé de l’inclure dans l’étendue des tests end-to-end, ceci permettant au final de tester la non-régression de l’ensemble de l’architecture (Hydroportail + API client).

Architecture Cypress

 

Voici l’approche qui a été retenue pour adresser ces problématiques :
En raison des contraintes ci-dessus, nous avons décidé de jouer les tests sur un environnement où le composant d'API client est déjà fonctionnel : il existe avant les tests et il est conservé en fin d'exécution des tests pour la prochaine fois.
Des scripts permettant la création des données maîtrisées et fixes dédiées aux tests ont été implémentés, pour ne pas se reposer sur les données provenant du client qui peuvent être mouvantes d’une livraison à l’autre. Concrètement, cela signifie qu’à chaque nouvelle livraison de l’API client, ces scripts doivent être joués une fois pour préparer les données de test. Comme il ne s’agit ici que de la création et l’ajout de données, les appels sont faits depuis l’Hydroportail directement et utilisent l’API client pour toutes ces actions. Ce jeu de données dédié aux tests contient une cinquantaine d’entités, dont une dizaine d’utilisateurs avec des droits spécifiques et variés pour les tests.
Parallèlement à ce script d’initialisation, des scripts de retour arrière ont dû être ajoutés pour remettre les données de tests dans leur état initial avant le lancement des séries de tests fonctionnels. Il est principalement question ici de supprimer l’ensemble des données qui ont été ajoutées par les tests eux-mêmes. Cependant, comme l'application n’est pas prévue pour supprimer des données, le développement de ces scripts a été un peu plus complexe : chaque série de tests fait un appel à une API interne à l’Hydroportail qui a été implémentée spécifiquement pour répondre à ce besoin. Dès qu’une ressource de test doit être supprimée, le test sollicite un nettoyage via :

  • Un appel vers l’API interne à l’Hydroportail dédiée aux tests.
  • Cette dernière envoie ensuite une requête vers l'application de l’API client sur laquelle des scripts PHP spécialisés ont été déposés spécifiquement pour les besoins de tests. Ces derniers lancent des requêtes directement en base de données pour appliquer les changements nécessaires au nettoyage des données de tests. Note : toutes les entités de tests sont repérées grâce à des fragments de noms spécifiques aux tests et la liste des identifiants des entités impactées est retournée.
  • L’Hydroportail invalide alors le cache des ressources qui viennent d’être supprimées pour éviter d’en servir une version obsolète.
  • Le test peut ensuite poursuivre
Cypress - Scripts retour arrière

Difficultés liées à Cypress

Cypress a un fonctionnement fondamentalement asynchrone qui peut dérouter en première approche, surtout pour des développeurs habitués à des langages comme le PHP.
Toutes les étapes des tests sont des callbacks qui se déclenchent seulement lorsque le moment est opportun dans le déroulement du test, ce qui demande parfois une gymnastique de l’esprit pour que l’algorithme de certains tests complexes s’exécute dans le bon ordre.
D’autre part, le déroulement d’une série de tests est préparé par Cypress avant même l’appel à la fonction « beforeAll() » (appelée une fois avant de jouer les tests de la série). Ceci rend compliquée l’écriture d’une série de tests dont le nombre d’étapes et les données d’entrée seraient dépendants d’informations dynamiquement récupérées (via un appel à une source de données externe par exemple).
Code minimaliste d’illustration :

tests/000_async-KO.js :

Code - Test async KO

Dans l’exemple ci-dessus, Cypress va exécuter :

  • (1) L’initialisation de la variable « testItems » à un tableau vide.
  • (2) L’appel à « beforeAll(...) », mais ce dernier ne joue pas immédiatement la fonction anonyme qui lui est passée en paramètre, il la mémorise pour une exécution ultérieure au moment opportun (voir Cypress - commands Are Asynchronous).
  • (3) N’est pas exécuté pour le moment.
  • (4) La boucle forEach sur la variable « testItems » qui, à ce stade, est un tableau vide, donc l’étape (5) ne sera jamais exécutée.
  • Comme aucun cas de test (déclaré par un appel à « it(...) ») n’a été construit, l’étape (3) ne sera jamais exécutée non plus.

Cypress lève alors une erreur comme quoi aucun test n’a été trouvé :

Cypress : Vue - Pas de test

En résumé, si l’ordre d'exécution intuitif aurait été (1), (2), (3), (4) puis (5), l’ordre d'exécution effectif est en réalité (1), (2) puis (4).
Diverses solutions de contournement plus ou moins élégantes existent pour pallier cette limitation. Au moment où nos tests ont été implémentés, la génération d’un fichier de configuration à partir d’un template semblait être une bonne approche car elle génère une configuration statique qui évite les requêtes à chaque lancement des tests.
Dans notre cas, comme les données de tests sont créées par un script de l’Hydroportail sur une base de données dont l’état n’est pas maîtrisé, la liste et les identifiants des entités de tests peuvent varier d’une version à l’autre de la machine virtuelle utilisée pour l’API client. Or, ces identifiants sont automatiquement attribués par cette API client et explicitement utilisés par les utilisateurs pour créer des références entre entités. Résultat : il est nécessaire que les tests connaissent ces identifiants avant de s’exécuter.
Une commande NodeJS a été développée spécifiquement pour générer le fichier de configuration utilisé par nos tests Cypress, à partir d’un template dans lequel des placeholders sont remplacés par les bons identifiants à utiliser. Cette commande est à jouer après la génération des données de tests par l’hydroportail, à chaque nouvelle version de la machine virtuelle de l’API client.
Une approche alternative est d’injecter les données nécessaires dans les variables d’environnement de Cypress, au lancement des tests. Pour ce faire, il nous a fallu développer un mini-plugin qui initialise les données nécessaires au lancement de Cypress :
plugins/index.js :

Cypress

Ceci permet d’utiliser ces données dans les fichiers de tests. Voici comment le fichier de test « 000_async-KO.js » peut être corrigé pour fonctionner correctement :
tests/001_async-OK.js :

Cypress

D’autre part, dans de rares cas, certaines problématiques ont été rencontrées car Cypress, bien que se voulant proche des utilisateurs en termes de comportement, ne sait pas toujours bien utiliser les widgets JavaScripts pour lesquels nous avons dû développer des commandes Cypress spécifiques (par exemples : un select avancé « bootstrap-select », un composant de sélection de date et heure « pc-bootstrap4-datetimepicker », etc.).

Les atouts de Cypress

Pendant les développements des tests Cypress, un certain nombre de points positifs et de forces de cet outil nous ont facilité la vie et valent le coup d’être mentionnés.

Pilotage de navigateurs complets

Cypress joue les tests en pilotant un navigateur complet (Chrome ou Firefox) ce qui lui confère un avantage sur certaines solutions alternatives car il évolue sur une application qui fonctionne de façon nominale, avec tous les scripts JavaScript actifs.
Ceci permet de se rapprocher au plus près des conditions réelles d’utilisation du site, mais aussi aux scripts de tests d’accéder au contexte d’exécution JavaScript si besoin.

Comportement naturel

D’autre part, Cypress a été conçu de façon à réagir de façon similaire au comportement d’un utilisateur humain pour les actions les plus courantes. Par exemple :

  • Lorsqu’on lui demande de cliquer sur un lien ou un élément, Cypress va attendre jusqu’à ce que cet élément soit présent. Il n’est pas nécessaire d’implémenter d’attentes ou de recherches manuellement, Cypress attend naturellement que l’élément apparaisse comme le ferait un utilisateur normal. Si cet élément met trop de temps à apparaître, alors Cypress abandonne (tout comme le ferait un utilisateur) et le test échoue. Ce timeout est bien entendu configurable.
  • Si l’élément sur lequel le test doit cliquer est présent dans le DOM, mais est inaccessible sur la page (à cause d’un style CSS, ou parce qu’il est sous un autre élément), alors Cypress lèvera une erreur car il n’aura pas “réussi” à cliquer sur cet élément. Ceci permet de repérer des soucis/régressions d’affichage ou de mise en page qui rendraient certaines fonctionnalités inaccessibles.
  • Si l’élément avec lequel interagir est en dehors de la vue courante, Cypress effectue un auto-scroll vers cet élément avant de continuer.
  • Lorsqu’une saisie est demandée dans un champ texte, elle est faite caractère par caractère, ce qui permet aux éventuels événements Javascript de se déclencher correctement.

Tout est en Javascript

Les tests Cypress s’écrivent en JavaScript et son API est relativement facile d’accès. Même une équipe de développeurs back-end PHP a réussi à la prendre en main !
Comme Cypress est spécialisé dans le test des sites Web dont l’unique langage de script est le JavaScript, l’ensemble technique reste cohérent. De ce fait, les développeurs front-end qui ont déployé l’interface Web peuvent sans peine écrire les tests fonctionnels du site. Comme les compétences en JavaScript ne font pas défaut dans notre industrie, ceci rend cet outil accessible à toutes les entreprises qui développent des sites Web.
Étendre Cypress est également possible avec l’ajout de commandes personnalisées ou de plug-ins en JavaScript également.
Enfin, le back-end de Cypress étant du NodeJS, il est possible de tirer parti de fonctionnalités système au besoin : nous ne sommes pas limités par le Javascript d’un contexte navigateur.

Développement interactif des tests

Cypress possède une interface interactive de lancement et de suivi des tests extrêmement utile pendant leurs développements.

Cypress

Lors de son exécution, chaque série de tests est détaillée sur la gauche, étape par étape pour montrer ce que Cypress est en train de tester, comment ça se déroule, quelles étapes échouent ou non, et pourquoi. Et parallèlement à cela, une vue sur ce qu’il se passe réellement sur le site testé s’anime en temps réel au fil des tests sur le panneau de droite.

Cypress hydroportail Schapi

Une fois une série de tests déroulée, il est possible de survoler chaque étape pour visualiser l’état du site à ce moment-là et mieux comprendre ce qui a fait échouer un test ou comment un test réussi s’est déroulé.

Exécution des tests dans un contexte automatisé

Une fois les tests implémentés, ils peuvent être exécutés entièrement en mode “headless”, sans aucune interface graphique. C’est particulièrement utile pour jouer les tests dans le contexte d’une plateforme d’intégration continue par exemple.
De façon générale, l’exécution des tests en ligne de commande permet de les jouer de façon plus rapide et avec un impact moindre sur la mémoire vive et le processeur. En effet, lorsqu’ils sont nombreux, l’exécution de la suite complète des tests peut représenter un temps d’exécution important.
Dans ce cas, un compte-rendu textuel est fait dans la console pour rendre compte de ce qui a réussi ou non. Par exemple :

Cypress spec

Mais surtout, Cypress peut optionnellement enregistrer tous les tests sous la forme de vidéos permettant de visualiser après coup ce qu’il s’est passé en cas de problème, chaque échec étant également accompagné d’une copie d’écran au format image. Cela fait gagner un temps précieux pour cibler et investiguer un échec lors de l’exécution des tests sur un serveur distant !

Conclusion

Comme toujours, le développement de tests en parallèle d’un projet demande un investissement conséquent et vient avec son lot de difficultés liées au projet lui-même et aux limitations des outils de tests. La prise en main d’un nouveau framework de test demande aussi une période d’apprentissage.
Néanmoins, s’il ne constitue pas une solution miracle, l’équipe a trouvé que Cypress règle une grande partie des difficultés inhérentes aux contraintes des tests d’applications Web et qu’une fois ses stratégies bien comprises, l’expérience générale est positive.
Au final, 41 séries de tests ont été implémentés totalisant plus de 1120 cas de tests qui couvrent les fonctionnalités principales de l’application. L’exécution complète du jeu de tests prend environ 50 minutes lorsqu’elle est exécutée sur notre plateforme de test configurée en mode production. C’est bien plus long sur un poste de développement.
Compte-tenu de notre expérience positive avec Cypress, nous envisageons de le réutiliser sur de futurs projets.

Formations associées

Front-end

Développement d'applications JavaScript

A distance (foad) Du 25 au 28 janvier 2022

Voir la formation

Actualités en lien

29/03/2019

Des boucles de composants génériques avec Angular

Ou comment faire des composants de listes réutilisables avec n'importe quel objet.

Voir l'article
Image
IA_Angular_Universal
08/10/2018

Mettre en place Angular Universal avec Angular 6 et 7

Le fonctionnement d'Angular Universal expliqué. Toutes les étapes de mise en place détaillées. Les pièges à éviter.

Voir l'article
Image
TypeScript
09/08/2018

Les nouveautés de Typescript 3.0

Typescript 3.0 vient de sortir, voici quelques nouveautés... et des exemples !

Voir l'article

Inscription à la newsletter

Nous vous avons convaincus