Quel développeur ou administrateur système n'a pas connu un moment où un client appelle et explique que son application en ligne ne fonctionne pas correctement, alors que quand vous vous y rendez, tout fonctionne bien ? Vous constatez que les pages répondent correctement, dans un délai acceptable, que le serveur n'est pas surchargé, mais vous remarquez dans l'historique que le code a été changé récemment.
Vous vous rendez sur la page qui charge la fonctionnalité qui, au dire du client, ne fonctionne pas correctement: il s'agit d'un formulaire d'authentification (mais pourtant vous avez essayé, et vous avez réussi à vous authentifier). Vous savez qu'un essai n'est pas un test, qu'un test effectue des mesures qu'avec votre essai vous ne pouvez pas réaliser, autrement qu'avec vos yeux et le débogueur du navigateur.
Dans le cas de notre application, il faut savoir qu'elle tournait en production dans un environnement "haute disponibilité", c’est-à-dire que le serveur en front est redondé sur une autre machine, et qu'un équilibreur de charge s'occupe de la répartition du trafic. Nous avions remarqué qu'une portion de code avait été modifiée, et en y regardant de plus près, ce code contenait un bug qui n'était présent que sur l'un des frontaux. Ainsi, parfois l'erreur se produisait, et parfois non.
Les tests de bout en bout (E2E tests) seraient parvenus à déceler l'erreur: dans le cas d'une architecture haute dispo, les 2 frontaux doivent être testés indépendamment l'un de l'autre. Les tests ne doivent pas passer par le loadbalancer, mais par les urls des frontaux directement. Pour un humain, c'est une tâche rébarbative que de jouer un document de recette sur chacun des frontaux, mais pas pour un script. Et c'est encore mieux si ce script est lancé automatiquement, idéalement après chaque commit qui a un impact sur le fonctionnement de l'application. Les tests fonctionnels permettent de tester toute la pile système , et il vaut donc mieux les lancer dans un environnement qui reproduit la production (le staging) mais aussi en production (utile dans le cas évoqué dans cet artice).
Il existe de nombreux outils bien connus tels que Selenium pour automatiser des tests ( lire notre article sur les tests avec browserless ), mais pour cet exemple, nous allons prendre 2 autres librairies Puppeteer et Playwright (qui a le vent en poupe et semble prendre la suite de Puppeteer).
Ces frameworks de tests fonctionnels permettent de tester le rendu d'une page web dans des conditions réelles, proches de celles d'un utlisateur. Ceci permet de contrôler que des éléments sont correctement affichés, de cliquer sur un lien ou un bouton, voir même dans une zone de l'écran ou sur un pixel précis. Cela permet égualement de remplir un formulaire, le poster ou encore de remplir le panier d'un site ecommerce. Ces outils permettent d'analyser le rendu de la page web tant coté backend (serveur) que frontend (navigateur). Il est ainsi tout a fait possible de tester aussi bien des applications PHP/Python/Ruby/Node que des applications Javascript utilisant des frameworks tels que React et VueJs, pour ne citer qu'eux.
Pour notre cas d'usage, les tests doivent s'exécuter dans le cadre d'une chaîne d'intégration continue (CI), mise en place sur runner Gitlab qui instancie un shell. Nous aurons besoin de Docker afin de construire l'image de l'instance NodeJS qui va éxecuter Puppeteer ou Playwright.
Dans les 2 cas, notre CI va jouer un scénario écrit en Javascript, orchestré par la librairie Jest vers un environnement de staging. Les tests fonctionnels doivent être bloquants afin que la CI s'arrête en cas d'erreur, avant de déployer l'application en production.
Puppeteer:
L'écriture du test avec Puppeteer prévoit l'utilisation d'un navigateur headless: nous allons utiliser l'image officielle Docker de Browserless.
Le scénario est simple, nous allons créer le fichier browser.test.js
et écrire le test qui vérifie que dans la page du site https://bearstech.com , on trouve bien le mot "Bearstech":
describe("TESTING", () => { beforeAll(async () =>; { if (!process.env.URL) { throw "ENV key URL is missing"; } const url = process.env.URL.startsWith("https://") ? process.env.URL : `https://${process.env.URL}`; await page.goto(url); }); it('should display "Bearstech" text on page', async () => { await expect(page).toMatch("Bearstech"); }); });