Test de régression visuelle avec Cypress : une approche pragmatique
Publié: 2022-03-11Chaque fois qu'une nouvelle version de notre bibliothèque de composants, Picasso, est publiée, nous mettons à jour toutes nos applications frontales pour tirer le meilleur parti des nouvelles fonctionnalités et aligner nos conceptions sur toutes les parties de notre site.
Le mois dernier, nous avons déployé une mise à jour Picasso sur le Toptal Talent Portal, la plateforme que nos talents utilisent pour trouver des emplois et interagir avec les clients. Sachant que la version viendrait avec des changements de conception majeurs, et dans un effort pour minimiser les problèmes inattendus, il était logique d'utiliser des techniques de test de régression visuelle pour nous aider à trouver les problèmes avant la sortie.
Les tests de régression visuelle ne sont pas un nouveau concept ; de nombreux autres projets chez Toptal l'utilisent déjà, y compris Picasso lui-même.
Des outils tels que Percy, Happo et Chromatic peuvent être utilisés pour aider les équipes à créer un pipeline de régression visuelle sain, et nous avons d'abord envisagé de les ajouter. Nous avons finalement décidé que le processus de configuration prendrait trop de temps et pourrait faire dérailler notre calendrier. Nous avions déjà une date fixée pour un gel du code pour commencer la migration, et avec seulement quelques jours avant la date limite, nous n'avions pas d'autre choix que d'être créatifs.
Tests de régression visuelle via des tests d'interface utilisateur
Bien que nous n'ayons pas eu de tests de régression visuelle dans le projet, nous avons eu une bonne couverture des tests d'intégration de l'interface utilisateur à l'aide de Cypress. Même si ce n'est pas pour cela que l'outil est principalement utilisé, Cypress a une page dans sa documentation dédiée aux tests visuels et une autre qui répertorie tous les plug-ins disponibles pour aider à configurer Cypress pour les tests visuels.
Du cyprès aux captures d'écran
Après avoir parcouru la documentation disponible, nous avons décidé d'essayer cypress-snapshot-plugin. Cela n'a pris que quelques minutes à mettre en place, et une fois que nous l'avons fait, nous avons rapidement réalisé que nous n'étions pas à la recherche d'une sortie de régression visuelle traditionnelle.
La plupart des outils de régression visuelle aident à identifier les changements indésirables en comparant des instantanés et en détectant les différences de pixels entre une ligne de base connue et acceptée et la version modifiée d'une page ou d'un composant. Si la différence de pixels est supérieure à un seuil de tolérance défini, la page ou le composant est marqué pour être examiné manuellement. Dans cette version, cependant, nous savions que nous allions apporter plusieurs petites modifications à la plupart de nos composants d'interface utilisateur, donc la définition d'un seuil n'était pas applicable. Même si un composant donné était différent à 100 %, il pourrait toujours être correct dans le contexte de la nouvelle version. De même, un écart aussi petit que quelques pixels peut signifier qu'un composant n'est actuellement pas adapté à la production.
À ce stade, deux choses contrastées sont devenues claires : noter les différences de pixels n'allait pas aider à identifier les problèmes, et avoir une comparaison côte à côte des composants était précisément ce dont nous avions besoin. Nous avons mis de côté le plug-in d'instantané et avons décidé de créer une collection d'images avec nos composants avant et après l'application de la mise à jour de Picasso. De cette façon, nous avons pu parcourir rapidement tous les changements pour déterminer si les nouvelles versions correspondaient toujours aux besoins du site et aux normes de la bibliothèque.
Le nouveau plan était de prendre une capture d'écran d'un composant, de la stocker localement, de prendre une nouvelle capture d'écran du même composant dans la branche avec la version mise à jour de Picasso, puis de les fusionner en une seule image. Au final, cette nouvelle approche n'était pas trop différente de ce que nous avions commencé, mais elle nous a donné plus de flexibilité lors de la phase d'implémentation puisque nous n'avions plus besoin d'importer le plug-in et d'utiliser ses nouvelles commandes.
Exploiter les API pour les images de comparaison
Avec un objectif clair en tête, il était temps de voir comment Cypress pouvait nous aider à obtenir les captures d'écran dont nous avions besoin. Comme mentionné, nous avons eu une bonne quantité de tests d'interface utilisateur couvrant la majorité du portail des talents, donc dans le but de collecter autant de composants critiques que possible, nous avons décidé de prendre des captures d'écran d'éléments individuels après chaque interaction.
Une approche alternative aurait été de prendre des captures d'écran de la page entière à des moments clés du test, mais nous avons décidé que ces images seraient trop difficiles à comparer. En outre, de telles comparaisons pourraient être plus sujettes à l'erreur humaine, comme manquer qu'un pied de page ait changé.
Une troisième option aurait été de parcourir chaque cas de test pour décider quoi capturer, mais cela aurait pris beaucoup plus de temps, donc s'en tenir à tous les éléments utilisés sur les pages semblait être un compromis pratique.
Nous nous sommes tournés vers l'API de Cypress pour générer les images. La commande cy.screenshot() peut, prête à l'emploi, créer des images individuelles de composants, et l'API After Screenshot nous permet de renommer des fichiers, de changer de répertoire et de distinguer les exécutions de régression visuelle des exécutions standard. En combinant les deux, nous avons créé des exécutions qui n'ont pas affecté nos tests fonctionnels et nous ont permis de stocker les images dans leurs dossiers appropriés.
Tout d'abord, nous avons étendu le fichier index.js dans notre répertoire de plug-ins pour prendre en charge les deux nouveaux types d'exécution (baseline et comparaison). Ensuite, nous définissons le chemin de nos images en fonction du type d'exécution :

// plugins/index.js const fs = require('fs') const path = require('path') module.exports = (on, config) => { // Adding these values to your config object allows you to access them in your tests. config.env.baseline = process.env.BASELINE || false config.env.comparison = process.env.COMPARISON || false on('after:screenshot', details => { // We only want to modify the behavior of baseline and comparison runs. if (config.env.baseline || config.env.comparison) { // We keep track of the file name and number to make sure they are saved in the proper order and in their relevant folders. // An alternative would have been to look up the folder for the latest image, but this was the simpler approach. let lastScreenshotFile = '' let lastScreenshotNumber = 0 // We append the proper suffix number to the image, create the folder, and move the file. const createDirAndRename = filePath => { if (lastScreenshotFile === filePath) { lastScreenshotNumber++ } else { lastScreenshotNumber = 0 } lastScreenshotFile = filePath const newPath = filePath.replace( '.png', ` #${lastScreenshotNumber}.png` ) return new Promise((resolve, reject) => { fs.mkdir(path.dirname(newPath), { recursive: true }, mkdirErr => { if (mkdirErr) { return reject(mkdirErr) } fs.rename(details.path, newPath, renameErr => { if (renameErr) { return reject(renameErr) } resolve({ path: newPath }) }) }) }) } const screenshotPath = `visualComparison/${config.env.baseline ? 'baseline' : 'comparison'}` return createDirAndRename(details.path .replace('cypress/integration', screenshotPath) .replace('All Specs', screenshotPath) ) } }) return config } Ensuite, nous avons invoqué chacune des exécutions en ajoutant la variable d'environnement correspondante à l'appel Cypress dans le package.json du projet :
"scripts": { "cypress:baseline": "BASELINE=true yarn cypress:open", "cypress:comparison": "COMPARISON=true yarn cypress:open" }Une fois que nous avons exécuté nos nouvelles commandes, nous avons pu voir que toutes les captures d'écran prises pendant l'exécution ont été déplacées vers les dossiers appropriés.
Ensuite, nous avons essayé de remplacer cy.get() , la commande principale de Cypress pour renvoyer les éléments DOM, et de prendre une capture d'écran de tous les éléments appelés avec son implémentation par défaut. Malheureusement, cy.get() est une commande difficile à modifier, car l'appel de la commande d'origine dans sa propre définition conduit à une boucle infinie. L'approche suggérée pour contourner cette limitation consiste à créer une commande personnalisée distincte, puis à faire en sorte que cette nouvelle commande prenne une capture d'écran après avoir trouvé l'élément :
Cypress.Commands.add("getAndScreenshot", (selector, options) => { // Note: You might need to tweak the command when getting multiple elements. return cy.get(selector).screenshot() }); it("get overwrite", () => { cy.visit("https://example.cypress.io/commands/actions"); cy.getAndScreenshot(".action-email") }) Cependant, nos appels pour interagir avec les éléments de la page étaient déjà enveloppés dans une fonction interne getElement() . Donc, tout ce que nous avions à faire était de nous assurer qu'une capture d'écran a été prise lorsque le wrapper a été appelé.
Résultats obtenus via des tests de régression visuelle
Une fois que nous avions les captures d'écran, il ne restait plus qu'à les fusionner. Pour cela, nous avons créé un simple script de nœud en utilisant Canvas. Au final, le script nous a permis de générer 618 images de comparaison ! Certaines des différences étaient faciles à repérer en ouvrant le portail des talents, mais certains des problèmes n'étaient pas aussi évidents.
Ajouter de la valeur aux tests d'interface utilisateur
Tout d'abord, les tests de régression visuelle ajoutés se sont avérés utiles et ont révélé quelques problèmes que nous aurions pu manquer sans eux. Même si nous nous attendions à des différences dans nos composants, savoir ce qui avait réellement changé a aidé à réduire les cas problématiques. Donc, si votre projet a une interface mais que vous n'effectuez pas encore ces tests, allez-y !
La deuxième leçon ici, et peut-être la plus importante, est qu'on nous a rappelé une fois de plus que la perfection est l'ennemie du bien. Si nous avions exclu la possibilité d'exécuter des tests de régression visuelle pour cette version car il n'y avait pas de configuration préalable, nous aurions peut-être manqué quelques bugs lors de la migration. Au lieu de cela, nous nous sommes mis d'accord sur un plan qui, bien qu'il ne soit pas idéal, était rapide à exécuter, nous y avons travaillé et cela a porté ses fruits.
Pour plus de détails sur la mise en œuvre d'un pipeline de régression visuelle robuste dans votre projet, veuillez vous référer à la page de test visuel de Cypress, sélectionnez l'outil qui correspond le mieux à vos besoins et regardez les vidéos du didacticiel.
