Adloun

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 dist et 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.

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
  },
});
Définition 11.1jsdom

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.

Proposition 11.2Structure d'un test

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%)
Proposition 11.3Vue Test Utils

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) et findAll, qui renvoient d'autres enveloppes ;
  • text(), html(), attributes(), classes(), et element, 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 cours

En accès libre, sans inscription.

Exercices corrigés de ce chapitre

Lire la suite du cours →