Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

Chapitre 1418 minDébutant

Tester avec Vitest et passer à TypeScript

Par Josselyn C., prof de BTS SIO. Mis à jour le .

À la fin de ce chapitre, tu sauras

  • écrire et lancer un test unitaire avec Vitest ;
  • choisir les cas à tester, cas limites compris ;
  • séparer le code testable du code qui touche au DOM ;
  • dire ce que TypeScript apporte et typer une fonction simple.

3 exercices corrigés

Un test unitaire est un petit programme qui appelle une fonction avec des données choisies et vérifie qu'elle renvoie le résultat attendu ; on le relance après chaque modification pour s'assurer que rien n'a cassé. Ce chapitre montre comment tester les règles de Gestock avec Vitest, l'outil de test du monde Vite, puis comment TypeScript, JavaScript complété par des types, détecte une partie des erreurs avant même l'exécution. Les principes généraux des tests (types de tests, cas de test, non-régression, couverture) sont enseignés dans le cours Tests unitaires et non-régression ; ici, on se concentre sur leur mise en œuvre en JavaScript.

Pourquoi tester ses fonctions

Tu as écrit validerPiece au chapitre 8. Pour vérifier qu'elle marche, tu remplis le formulaire avec une bonne saisie, puis une référence en double, puis une quantité négative, puis un prix vide… Chaque modification du code t'oblige à tout refaire à la main, et tu finis par sauter des cas. Un test automatique fait ces vérifications en quelques millisecondes, à chaque enregistrement.

Les tests apportent trois choses :

  • la non-régression : une modification qui casse un comportement existant est détectée tout de suite ;
  • une documentation vivante : la liste des tests décrit ce que la fonction doit faire, cas limites compris ;
  • une preuve pour l'E6 : montrer au jury une suite de tests qui passe, et expliquer pourquoi tu as choisi ces cas, compte dans l'évaluation de la qualité de ta réalisation (épreuve E6 SLAM).

Le découpage du chapitre 11 paie ici : stock.js ne contient que des fonctions pures, sans DOM ni réseau. Elles se testent directement dans Node.js, sans navigateur.

Installer Vitest

Dans le projet créé avec Vite :

bash
npm install --save-dev vitest

Ajoute un script dans package.json :

json
{
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "test": "vitest"
  }
}

npm test lance Vitest en mode surveillance : il relance les tests concernés à chaque enregistrement d'un fichier. npx vitest run les lance une seule fois, comme le fera un serveur d'intégration continue (cours Intégration continue). Vitest trouve tout seul les fichiers dont le nom finit par .test.js.

Écrire un premier test

Le fichier de test se place à côté du fichier testé : js/stock.test.js.

js
import { describe, test, expect } from 'vitest';
import { estEnAlerte, valeurStock, ajusterQuantite, validerPiece, lireNombre } from './stock.js';

const pieces = [
  { ref: 'SSD-512', nom: 'SSD NVMe 512 Go', categorie: 'Stockage', quantite: 12, seuil: 5, prix: 42.9 },
  { ref: 'RAM-16', nom: 'Barrette DDR4 16 Go', categorie: 'Mémoire', quantite: 3, seuil: 4, prix: 38.5 },
];

describe('estEnAlerte', () => {
  test('alerte quand la quantité est sous le seuil', () => {
    expect(estEnAlerte({ quantite: 3, seuil: 4 })).toBe(true);
  });
  test('alerte quand la quantité est égale au seuil (cas limite)', () => {
    expect(estEnAlerte({ quantite: 4, seuil: 4 })).toBe(true);
  });
  test("pas d'alerte au-dessus du seuil", () => {
    expect(estEnAlerte({ quantite: 5, seuil: 4 })).toBe(false);
  });
});

describe('valeurStock', () => {
  test('additionne quantité × prix', () => {
    expect(valeurStock(pieces)).toBeCloseTo(630.3);
  });
  test('vaut 0 pour un stock vide', () => {
    expect(valeurStock([])).toBe(0);
  });
});

describe('ajusterQuantite', () => {
  test('ne descend jamais sous zéro', () => {
    const resultat = ajusterQuantite(pieces, 'RAM-16', -10);
    expect(resultat[1].quantite).toBe(0);
  });
  test('ne modifie pas le tableau reçu', () => {
    ajusterQuantite(pieces, 'SSD-512', 1);
    expect(pieces[0].quantite).toBe(12);
  });
});

describe('validerPiece', () => {
  const saisieValide = { ref: 'ven-12', nom: 'Ventilateur', categorie: 'Consommable', quantite: '4', seuil: '2', prix: '7,90' };

  test('accepte une saisie correcte', () => {
    expect(validerPiece(saisieValide, pieces)).toEqual({});
  });
  test('refuse une référence déjà utilisée', () => {
    const erreurs = validerPiece({ ...saisieValide, ref: 'SSD-512' }, pieces);
    expect(erreurs.ref).toMatch(/existe déjà/);
  });
  test.each(['', '-1', '2.5', 'abc'])('refuse la quantité « %s »', (quantite) => {
    expect(validerPiece({ ...saisieValide, quantite }, pieces)).toHaveProperty('quantite');
  });
});

test('lireNombre accepte la virgule décimale', () => {
  expect(lireNombre('7,90')).toBe(7.9);
  expect(lireNombre('')).toBeNaN();
});

Chaque test a un nom qui dit le comportement attendu, appelle la fonction, et vérifie le résultat avec expect(valeur) suivi d'un comparateur (matcher). describe regroupe les tests d'une même fonction. test.each exécute le même test pour plusieurs valeurs : ici, quatre saisies de quantité invalides.

ComparateurVérifie que…
toBe(x)la valeur est identique à x (Object.is) : nombres, chaînes, booléens
toEqual(obj)un objet ou un tableau a le même contenu (comparaison en profondeur)
toBeCloseTo(n)un décimal est égal à n, aux erreurs d'arrondi près
toMatch(/regex/)une chaîne correspond au motif
toHaveProperty('cle')l'objet a cette propriété
toHaveLength(n)le tableau ou la chaîne a cette longueur
toBeNaN(), toBeNull(), toBeUndefined()valeur spéciale
toThrow(/message/)la fonction lève une erreur ; s'écrit expect(() => f()).toThrow()
.notinverse le comparateur : expect(x).not.toBe(0)

Lancer les tests et lire le résultat

bash
npx vitest run
text
 ✓ js/stock.test.js (14 tests) 2ms

 Test Files  1 passed (1)
      Tests  14 passed (14)

Quatorze tests : trois pour estEnAlerte, deux pour valeurStock, deux pour ajusterQuantite, six pour validerPiece (dont les quatre de test.each) et un pour lireNombre. Introduis maintenant volontairement un bug : remplace <= par < dans estEnAlerte. Vitest le signale aussitôt, avec le test concerné, la valeur attendue, la valeur reçue et la ligne :

text
 FAIL  js/stock.test.js > estEnAlerte > alerte quand la quantité est égale au seuil (cas limite)
AssertionError: expected false to be true // Object.is equality

- Expected
+ Received

- true
+ false

 ❯ js/stock.test.js:14:52

C'est précisément le cas limite (quantité égale au seuil) qui attrape l'erreur : les deux autres tests passent toujours. D'où l'importance de choisir les cas à tester.

Choisir les cas à tester

Pour chaque fonction, teste :

  1. le cas normal, le plus fréquent ;
  2. les cas limites : la valeur exactement égale au seuil, le tableau vide, la chaîne vide, zéro, la longueur maximale ;
  3. les cas d'erreur : chaque règle de validation doit avoir au moins un test qui la viole ;
  4. les effets indésirables : la fonction ne modifie-t-elle pas ce qu'elle ne doit pas modifier ?

Un test qui ne peut pas échouer ne sert à rien. Vérifie au moins une fois que chaque test échoue quand tu casses volontairement le code, comme ci-dessus.

Et le code qui touche au DOM ?

vue.js et main.js utilisent document, qui n'existe pas dans Node.js. Deux solutions :

  • Vitest peut simuler un navigateur avec un environnement jsdom ou happy-dom (à installer, puis // @vitest-environment jsdom en tête du fichier de test) : document.createElement fonctionne, et tu peux vérifier le contenu d'un tbody après afficherPieces.
  • Pour tester une application dans un vrai navigateur (cliquer, remplir un formulaire), on utilise des tests de bout en bout avec Playwright, ou le mode navigateur de Vitest.

Le plus efficace reste de mettre le maximum de logique dans des fonctions pures : ce qui reste dans vue.js et main.js est alors si simple qu'un test manuel suffit souvent.

TypeScript : JavaScript avec des types

JavaScript ne vérifie les types qu'à l'exécution, et souvent pas du tout : '3' <= 4 vaut true sans aucun avertissement. TypeScript, créé par Microsoft, est un langage qui ajoute à JavaScript des annotations de types, vérifiées avant l'exécution par le compilateur tsc. Le code TypeScript se transforme ensuite en JavaScript ordinaire, en retirant les types.

ts
// stock.ts
interface Piece {
  ref: string;
  nom: string;
  categorie: string;
  quantite: number;
  seuil: number;
  prix: number;
}

function estEnAlerte(piece: Piece): boolean {
  return piece.quantite <= piece.seuil;
}

function valeurStock(pieces: Piece[]): number {
  return pieces.reduce((total, p) => total + p.quantite * p.prix, 0);
}

Une interface décrit la forme d'un objet ; piece: Piece dit que le paramètre doit avoir cette forme ; : boolean après les parenthèses annonce le type du résultat. Si tu passes une pièce dont la quantité est restée une chaîne (la saisie d'un formulaire non convertie), le compilateur refuse le code :

ts
const saisie = { ref: 'RAM-16', quantite: '3', seuil: 4 };
estEnAlerte(saisie);
text
error TS2345: Argument of type '{ ref: string; quantite: string; seuil: number; }' is not assignable to parameter of type 'Piece'.
  Types of property 'quantite' are incompatible.
    Type 'string' is not assignable to type 'number'.

En JavaScript, ce même appel renverrait true sans broncher, et le bug apparaîtrait bien plus tard. VS Code affiche ces erreurs en direct, soulignées en rouge, pendant que tu tapes.

Le pas intermédiaire : @ts-check et JSDoc

Pas besoin de tout réécrire pour profiter de la vérification des types. Un commentaire // @ts-check en tête d'un fichier .js demande à VS Code de le vérifier, en lisant les types dans les commentaires JSDoc (chapitre 4) :

js
// @ts-check

/**
 * @typedef {{ ref: string, quantite: number, seuil: number }} Piece
 */

/**
 * @param {Piece} piece
 * @returns {boolean}
 */
export function estEnAlerte(piece) {
  return piece.quantite <= piece.seuil;
}

estEnAlerte({ ref: 'RAM-16', quantite: '3', seuil: 4 });
// VS Code souligne '3' : Type 'string' is not assignable to type 'number'.

Le fichier reste du JavaScript, qui s'exécute tel quel dans le navigateur. C'est une bonne façon d'essayer les types sur un projet existant.

Utiliser TypeScript dans un projet

SituationComment faire
Nouveau projet webnpm create vite@latest gestock -- --template vanilla-ts : Vite compile le TypeScript à la volée
Vérifier tous les types du projetnpx tsc --noEmit (Vite retire les types sans les vérifier : lance tsc dans tes scripts ou ta CI)
Script ou serveur Node.jsNode.js exécute directement un fichier .ts depuis les versions 22.18 et 24 (node stock.ts), en retirant les types sans les vérifier
Bibliothèque sans typesLes types de la plupart des paquets sont fournis par le paquet lui-même ou par @types/nom-du-paquet

Faut-il apprendre TypeScript tout de suite ? Non : TypeScript est du JavaScript, plus des types. Maîtrise d'abord le langage (ce cours), puis ajoute les types quand tes projets grossissent. Beaucoup d'entreprises démarrent aujourd'hui leurs nouveaux projets web en TypeScript, et React, Angular, Vue et Node.js s'utilisent couramment avec lui : c'est une compétence à mettre en avant dans ton portfolio.

Exercices

Exercice 1 sur 3 Écrire les tests de statut Réussi À revoir

La fonction statut(quantite, seuil) du chapitre 3 renvoie 'rupture', 'alerte' ou 'ok'. Écris une suite de tests Vitest qui couvre tous les cas, cas limites compris. Combien de tests faut-il au minimum ?

Voir la réponseMasquer la réponse de l'exercice 1
js
import { describe, test, expect } from 'vitest';
import { statut } from './stock.js';

describe('statut', () => {
  test('rupture quand la quantité vaut 0', () => {
    expect(statut(0, 2)).toBe('rupture');
  });
  test('rupture même si le seuil vaut 0', () => {
    expect(statut(0, 0)).toBe('rupture');
  });
  test('alerte quand la quantité est sous le seuil', () => {
    expect(statut(3, 4)).toBe('alerte');
  });
  test('alerte quand la quantité est égale au seuil', () => {
    expect(statut(4, 4)).toBe('alerte');
  });
  test('ok juste au-dessus du seuil', () => {
    expect(statut(5, 4)).toBe('ok');
  });
});

Au minimum trois tests, un par résultat possible. Mais les deux cas limites (quantité égale au seuil, et rupture avec un seuil nul) sont ceux qui détectent les erreurs de < au lieu de <= et d'ordre des conditions : cinq tests est un bon minimum.

Tu avais trouvé ?

Exercice 2 sur 3 Le test qui révèle le bug Réussi À revoir

Un collègue a écrit cette fonction et ce test, qui passe. Pourtant, en production, une remise de 0 % applique 10 %. Trouve le bug, écris le test qui l'aurait détecté, puis corrige.

js
export function prixRemise(prix, taux) {
  const t = taux || 10;
  return prix * (1 - t / 100);
}

test('applique la remise', () => {
  expect(prixRemise(100, 20)).toBe(80);
});
Voir la réponseMasquer la réponse de l'exercice 2

taux || 10 remplace aussi 0, qui est falsy, par 10 (chapitre 2). Le test ne couvre qu'un cas normal.

js
test('une remise de 0 % ne change pas le prix', () => {
  expect(prixRemise(100, 0)).toBe(100);   // échoue avec le code actuel : reçu 90
});
test('10 % par défaut si le taux est absent', () => {
  expect(prixRemise(100)).toBe(90);
});

Correction : const t = taux ?? 10;. Les deux nouveaux tests et l'ancien passent.

Tu avais trouvé ?

Exercice 3 sur 3 Annoter en TypeScript Réussi À revoir

Écris en TypeScript l'interface LigneCommande (référence, quantité, prix unitaire) et la fonction totalCommande qui reçoit un tableau de lignes et renvoie le total. Quelle erreur le compilateur signale-t-il pour l'appel totalCommande([{ ref: 'SSD-512', quantite: 2 }]) ?

Voir la réponseMasquer la réponse de l'exercice 3
ts
interface LigneCommande {
  ref: string;
  quantite: number;
  prix: number;
}

function totalCommande(lignes: LigneCommande[]): number {
  return lignes.reduce((total, l) => total + l.quantite * l.prix, 0);
}

L'appel est refusé parce que la propriété prix manque :

text
error TS2741: Property 'prix' is missing in type '{ ref: string; quantite: number; }' but required in type 'LigneCommande'.

En JavaScript, l'appel aurait renvoyé NaN (2 * undefined), sans aucun message.

Tu avais trouvé ?

Chapitre terminé !

Fais les 3 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Exercices JavaScript corrigés type examenRevoir le sommaire du cours