Tests et mise en production
Cours · Vue : construire une application web (école d'ingénieur), chapitre 11 · école d'ingénieur, informatique · le début du chapitre, la suite sur Adloun
La Librairie du Campus a maintenant un routeur, deux stores, une dizaine de composants. À chaque modification, on l'a vérifiée à la main : ouvrir le catalogue, chercher, trier, ajouter un livre, recharger. Ce contrôle prend du temps, on finit par le faire à moitié, et c'est justement la partie oubliée qui casse. Le livre JavaScript a présenté les tests automatiques avec node:test ; une application Vue a besoin d'un peu plus : un navigateur simulé pour monter les composants, de quoi déclencher un clic, attendre une mise à jour du DOM, remplacer le serveur.
Ce chapitre présente Vitest, l'outil de test de l'écosystème Vite, et Vue Test Utils, la bibliothèque officielle pour monter et manipuler un composant dans un test. On écrit les tests de la Librairie (fonctions, stores, composants, page), puis on fait relire le code par ESLint. La seconde moitié du chapitre conduit l'application jusqu'à un vrai serveur web : construction, contenu du dossier dist, mise en cache, réécriture des adresses, relais de l'API.
Objectifs du chapitre
- Écrire et lancer des tests avec Vitest, lire un rapport d'échec.
- Monter un composant, trouver un élément, déclencher un événement, vérifier le rendu et les événements émis.
- Tester ce qui est asynchrone : mise à jour du DOM, requêtes (simulées), minuteurs (temps simulé).
- Tester un store, un composable, et un composant qui dépend d'un store et du routeur.
- Configurer ESLint pour Vue et corriger ce qu'il signale.
- Construire l'application, comprendre le dossier
distet le déployer derrière un serveur web, routes et API comprises.
Versions utilisées : Vitest 5.0.3, Vue Test Utils 2.5.1, jsdom 29, ESLint 10 et eslint-plugin-vue 10.11. Toutes les sorties reproduites ont été produites par l'exécution des commandes indiquées ; seules les durées, mesurées en millisecondes, varient d'une exécution à l'autre.
11.1 Que tester, et comment ?
Un test automatique est un programme qui exécute une partie de l'application et vérifie un résultat attendu. On en distingue trois niveaux.
- Test unitaire une fonction, un store, un composable, isolés :
normaliser("L’Étranger")doit donner"l’etranger". Rapide, précis, facile à écrire. - Test de composant un composant monté dans un navigateur simulé : on clique sur son bouton, on vérifie ce qu'il affiche et ce qu'il émet.
- Test de bout en bout l'application construite, ouverte dans un vrai navigateur et pilotée comme par un utilisateur (exercice 11.10). Le plus réaliste, le plus lent.
On écrit beaucoup de tests du premier niveau, une bonne part du deuxième, et quelques parcours essentiels du troisième. Un test vérifie un comportement visible (ce qu'affiche le composant, ce qu'il émet, ce que contient le store), jamais un détail d'écriture : un test qui casse quand on renomme une variable interne n'aide personne.
11.2 Vitest
Vitest réutilise la configuration de Vite : les alias comme @, le traitement des fichiers .vue, tout fonctionne dans les tests comme dans l'application. On installe les outils de test en dépendances de développement :
$ npm install -D vitest@5.0.3 @vue/test-utils@2.5.1 jsdom@29.1.1
et l'on ajoute une section test à vite.config.js :
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
export default defineConfig({
plugins: [vue()],
test: {
environment: "jsdom", // un faux navigateur : document, window, événements
reporters: ["tree"], // un test par ligne
},
});
jsdom est une implémentation du DOM écrite en JavaScript : il fournit document, window, les éléments, les événements, localStorage… dans Node.js, sans fenêtre ni moteur de rendu. C'est assez pour monter un composant et interagir avec lui ; ce n'est pas assez pour mesurer une mise en page (jsdom ne calcule ni tailles ni positions).
Les tests sont des fichiers dont le nom se termine par .test.js. npx vitest les exécute, puis les relance à chaque modification d'un fichier (mode « surveillance ») ; npx vitest run les exécute une seule fois, ce qui convient à un serveur d'intégration continue.
La fonction test déclare un test : elle reçoit son nom et la fonction à exécuter. describe regroupe des tests sous un nom commun. Dans un test, expect(valeur) suivi d'un comparateur énonce ce qui doit être vrai :
toBe: égalité stricte ;toEqual: même contenu, pour les objets et les tableaux ;toHaveLength,toContain,toBeDefined,toBeUndefined,toHaveBeenCalledTimes, et bien d'autres.
La fonction de test peut être async : Vitest attend sa promesse. beforeEach et afterEach s'exécutent avant et après chaque test du groupe.
11.3 Tester un composant
Le projet code/ch11/demo-tests/ contient un petit composant de choix de quantité, lié par v-model :
<script setup>
const quantite = defineModel({ type: Number, default: 1 });
const props = defineProps({
max: { type: Number, default: 10 },
});
function changer(pas) {
quantite.value = Math.min(props.max, Math.max(1, quantite.value + pas));
}
</script>
<template>
<div class="quantite">
<button
type="button"
aria-label="Un de moins"
:disabled="quantite <= 1"
@click="changer(-1)"
>
−
</button>
<output aria-live="polite">{{ quantite }}</output>
<button
type="button"
aria-label="Un de plus"
:disabled="quantite >= max"
@click="changer(1)"
>
+
</button>
</div>
</template>
import { describe, test, expect } from "vitest";
import { mount } from "@vue/test-utils";
import ChoixQuantite from "../src/components/ChoixQuantite.vue";
describe("ChoixQuantite", () => {
test("affiche la quantité reçue", () => {
const wrapper = mount(ChoixQuantite, { props: { modelValue: 3 } });
expect(wrapper.find("output").text()).toBe("3");
});
test("le bouton + émet la nouvelle quantité", async () => {
const wrapper = mount(ChoixQuantite, { props: { modelValue: 3 } });
await wrapper.find("[aria-label='Un de plus']").trigger("click");
expect(wrapper.emitted("update:modelValue")).toEqual([[4]]);
});
test("le bouton − est désactivé à 1", () => {
const wrapper = mount(ChoixQuantite, { props: { modelValue: 1 } });
const moins = wrapper.find("[aria-label='Un de moins']");
expect(moins.attributes("disabled")).toBeDefined();
});
test("ne dépasse pas le maximum", async () => {
const wrapper = mount(ChoixQuantite, { props: { modelValue: 5, max: 5 } });
await wrapper.find("[aria-label='Un de plus']").trigger("click");
expect(wrapper.emitted("update:modelValue")).toBeUndefined();
});
});
RUN v5.0.3 …
✓ tests/ChoixQuantite.test.js (4 tests) 23ms
✓ ChoixQuantite (4)
✓ affiche la quantité reçue 15ms
✓ le bouton + émet la nouvelle quantité 5ms
✓ le bouton − est désactivé à 1 1ms
✓ ne dépasse pas le maximum 1ms
Test Files 1 passed (1)
Tests 4 passed (4)
Start at 20:29:07
Duration 534ms (environment 76%, import 11%, transform 8%, tests 5%, worker 1%)
mount(Composant, options) monte le composant dans le DOM de jsdom et renvoie une enveloppe (wrapper). Les options donnent ses props, ses slots, et dans global.plugins les plugins de l'application (Pinia, routeur). L'enveloppe offre :
find(sélecteur)etfindAll, qui renvoient d'autres enveloppes ;text(),html(),attributes(),classes(), etelement, l'élément du DOM lui-même ;trigger("click"),setValue("…"), qui renvoient une promesse, résolue après la mise à jour du DOM ;emitted(), la liste des événements émis et de leurs arguments.
On teste le composant comme son parent le voit : on lui donne une prop modelValue, et l'on attend un événement update:modelValue. Le composant ne modifie pas lui-même sa quantité (le parent la tient), d'où le test sur emitted plutôt que sur le texte affiché. On trouve les boutons par leur nom accessible (aria-label) : c'est ainsi que l'utilisateur d'un lecteur d'écran les trouve aussi.
11.3.1 Lire un échec
Un test n'a de valeur que s'il échoue quand le code est faux. Dans la copie du projet placée dans code/ch11/erreurs/test-rouge/, la même fonction changer a perdu sa borne maximale (Math.max(1, quantite.value + pas)). Le dernier test réagit :
La suite du cours est sur Adloun
Le chapitre en entier, formules et figures comprises, avec le tuteur, les fiches, les QCM et les exercices.
Lire la suite du coursEn accès libre, sans inscription.