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");
});
});
Ensuite, pour créer l'image qui va instancier Puppeteer, nous aurons besoin de l'installer (avec Jest) à partir du package.json:
{
"name": "functionnal-tests",
"version": "1.0.0",
"license": "MIT",
"scripts": {
"test": "jest"
},
"devDependencies": {
"jest": "^27.5.1",
"jest-html-reporter": "^3.4.2",
"jest-puppeteer": "^6.1.0",
"puppeteer": "^13.5.1"
}
}
Au même endroit, il nous faudra aussi:
le fichier jest-puppeteer.config.js qui sert à définir les paramètres de connexion au navigateur headless de Browserless
le fichier jest.config.js
Vous mettez ces 4 fichiers dans un répertoire "tests". Ensuite, il vous reste à écrire le Dockerfile qui doit charger une image NodeJS et copier notre dossier "tests" dans l'image. La commande à ajouter dans le Dockerfile est celle qui sera lancée par le conteneur pour exécuter le scénario: CMD ["yarn", "test"]
Pour plus de détail sur la réalisation de ce test, rendez-vous sur la documentation de notre Workflow DevOps
Playwright
C'est un nouveau framework dédié aux tests fonctionnels, maintenu par Microsoft, et qui prend en charge par défaut 3 navigateurs headless: Chromium, Firefox, et WebKit ainsi que plusieurs langages pour écrire les tests: python, node, java et .NET. Playwright propose ses propre images Docker officielles, mais comme chez Bearstech on aime bien comprendre ce que l'on fait, il nous semble plus efficace d'en créer une qui installe juste ce qu'il nous faut.
Jest a sorti un package encore en développement pour piloter playwright. Bien qu'il soit recommandé d'utiliser le "Playwright test-runner" depuis la page github du projet, on prend le risque d'utiliser Jest. Mais pas moyen d'utiliser un autre navigateur headless que ceux qui sont embarqués dans le framework. Les navigateurs sont patchés pour, d'après Playwright, mieux les piloter (ah Microsoft qui adore enfermer ses utilisateurs ...)
Notre scénario sera similaire au précédent, en ajoutant un test sur le click d'un bouton (il vaut mieux préciser ici un timeout à Jest):
[x] example.test.js
```javascript
// Needs to be higher than the default Playwright timeout
jest.setTimeout(40 * 1000)
describe("Bearstech.com", () => {
it("should have the text like 'Infogérance' in the h1", async () => {
const url = process.env.URL;
await page.goto(url);
await expect(page).toHaveText("h1", "Infogérance")
})
it("should navigate to contact once you click on 'lancer votre projet'", async () => {
const elements = await page.$$('a.button');
await elements[0].click();
expect(page.url()).toMatch(/contact/)
})
})
Les fichiers de configurations sont les suivants:
- [x] jest-playwright.config.js
```json
module.exports = {
browsers: ["chromium"],
exitOnPageError: false, // GitHub currently throws errors
launchOptions: {
headless: true
}
}
[x] jest.config.js
module.exports = {
verbose: true,
preset: 'jest-playwright-preset'
}
[x] package.json
{
"name": "jest-playwright-example",
"version": "1.0.0",
"license": "MIT",
"scripts": {
"test": "jest"
},
"devDependencies": {
"jest": "^27.5.1",
"jest-circus": "^27.0.0",
"jest-environment-node": "^27.0.0",
"jest-runner": "^27.0.0",
"jest-playwright-preset": "^1.7.2",
"playwright": "^1.22.2"
},
"dependencies": {}
}
Ces 4 fichiers doivent être placés dans un répertoire "tests".
Reste ensuite à écrire notre Dockerfile NodeJS, qui doit procéder à l'installation de Playwright et de Jest, puis indiquer au conteneur quelle commande exécuter à son instanciation:
FROM bearstech/node-dev:16
RUN mkdir /tests
COPY tests/ /tests/
WORKDIR /tests
RUN npm install jest jest-playwright-preset playwright-chromium
RUN npx playwright install
RUN npx playwright install-deps
CMD ["npm", "run", "test"]
Attention, le build est assez long (l'image a une taille de 2Go au final) à cause du navigateur headless.
Au final, il ne reste plus qu'à builder puis à lancer le conteneur:
docker run --rm -it -e URL="https://bearstech.com" --workdir /tests testplaywright:latest
Conclusion
Les tests fonctionnels peuvent vous sauver la mise et vous faire gagner du temps en prouvant la stabilité et en assurant la non régression d'une application. Pour les projets en cours de développement ou les projets matures, les tests font partie du cycle d'exploitation d'une application. Il faut pouvoir se doter d'outils qui permettent de prouver et tracer l'état dans lequel elle se trouve à chaque instant.
Chez Bearstech, nous administrons des applications depuis 15 ans, et nous avons mis au point un workflow qui vous permettra d'intégrer vos tests facilement, et accompagnés d'experts qui vous guideront dans la mise en place de ces outils. N'hésitez pas à revenir vers nous pour obtenir un avis d'expert sur la performance de vos applications, l'infogérance de vos serveurs, ou juste pour de l'aide pour la réalisation de ce guide :smile:
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");
});
});
Ensuite, pour créer l'image qui va instancier Puppeteer, nous aurons besoin de l'installer (avec Jest) à partir du package.json:
{
"name": "functionnal-tests",
"version": "1.0.0",
"license": "MIT",
"scripts": {
"test": "jest"
},
"devDependencies": {
"jest": "^27.5.1",
"jest-html-reporter": "^3.4.2",
"jest-puppeteer": "^6.1.0",
"puppeteer": "^13.5.1"
}
}
Au même endroit, il nous faudra aussi:
le fichier jest-puppeteer.config.js qui sert à définir les paramètres de connexion au navigateur headless de Browserless
le fichier jest.config.js
Vous mettez ces 4 fichiers dans un répertoire "tests". Ensuite, il vous reste à écrire le Dockerfile qui doit charger une image NodeJS et copier notre dossier "tests" dans l'image. La commande à ajouter dans le Dockerfile est celle qui sera lancée par le conteneur pour exécuter le scénario: CMD ["yarn", "test"]
Pour plus de détail sur la réalisation de ce test, rendez-vous sur la documentation de notre Workflow DevOps
Playwright
C'est un nouveau framework dédié aux tests fonctionnels, maintenu par Microsoft, et qui prend en charge par défaut 3 navigateurs headless: Chromium, Firefox, et WebKit ainsi que plusieurs langages pour écrire les tests: python, node, java et .NET. Playwright propose ses propre images Docker officielles, mais comme chez Bearstech on aime bien comprendre ce que l'on fait, il nous semble plus efficace d'en créer une qui installe juste ce qu'il nous faut.
Jest a sorti un package encore en développement pour piloter playwright. Bien qu'il soit recommandé d'utiliser le "Playwright test-runner" depuis la page github du projet, on prend le risque d'utiliser Jest. Mais pas moyen d'utiliser un autre navigateur headless que ceux qui sont embarqués dans le framework. Les navigateurs sont patchés pour, d'après Playwright, mieux les piloter (ah Microsoft qui adore enfermer ses utilisateurs ...)
Notre scénario sera similaire au précédent, en ajoutant un test sur le click d'un bouton (il vaut mieux préciser ici un timeout à Jest):
[x] example.test.js
```javascript
// Needs to be higher than the default Playwright timeout
jest.setTimeout(40 * 1000)
describe("Bearstech.com", () => {
it("should have the text like 'Infogérance' in the h1", async () => {
const url = process.env.URL;
await page.goto(url);
await expect(page).toHaveText("h1", "Infogérance")
})
it("should navigate to contact once you click on 'lancer votre projet'", async () => {
const elements = await page.$$('a.button');
await elements[0].click();
expect(page.url()).toMatch(/contact/)
})
})
Les fichiers de configurations sont les suivants:
- [x] jest-playwright.config.js
```json
module.exports = {
browsers: ["chromium"],
exitOnPageError: false, // GitHub currently throws errors
launchOptions: {
headless: true
}
}
[x] jest.config.js
module.exports = {
verbose: true,
preset: 'jest-playwright-preset'
}
[x] package.json
{
"name": "jest-playwright-example",
"version": "1.0.0",
"license": "MIT",
"scripts": {
"test": "jest"
},
"devDependencies": {
"jest": "^27.5.1",
"jest-circus": "^27.0.0",
"jest-environment-node": "^27.0.0",
"jest-runner": "^27.0.0",
"jest-playwright-preset": "^1.7.2",
"playwright": "^1.22.2"
},
"dependencies": {}
}
Ces 4 fichiers doivent être placés dans un répertoire "tests".
Reste ensuite à écrire notre Dockerfile NodeJS, qui doit procéder à l'installation de Playwright et de Jest, puis indiquer au conteneur quelle commande exécuter à son instanciation:
FROM bearstech/node-dev:16
RUN mkdir /tests
COPY tests/ /tests/
WORKDIR /tests
RUN npm install jest jest-playwright-preset playwright-chromium
RUN npx playwright install
RUN npx playwright install-deps
CMD ["npm", "run", "test"]
Attention, le build est assez long (l'image a une taille de 2Go au final) à cause du navigateur headless.
Au final, il ne reste plus qu'à builder puis à lancer le conteneur:
docker run --rm -it -e URL="https://bearstech.com" --workdir /tests testplaywright:latest
Conclusion
Les tests fonctionnels peuvent vous sauver la mise et vous faire gagner du temps en prouvant la stabilité et en assurant la non régression d'une application. Pour les projets en cours de développement ou les projets matures, les tests font partie du cycle d'exploitation d'une application. Il faut pouvoir se doter d'outils qui permettent de prouver et tracer l'état dans lequel elle se trouve à chaque instant.
Chez Bearstech, nous administrons des applications depuis 15 ans, et nous avons mis au point un workflow qui vous permettra d'intégrer vos tests facilement, et accompagnés d'experts qui vous guideront dans la mise en place de ces outils. N'hésitez pas à revenir vers nous pour obtenir un avis d'expert sur la performance de vos applications, l'infogérance de vos serveurs, ou juste pour de l'aide pour la réalisation de ce guide :smile: