Lesson 8/9 11 min

Your tests in a real project

The sandbox was for learning. Enter Vitest: install it, run your tests in watch mode, in CI, and read coverage without getting fooled.

Tu sais écrire un test. Mais qui le lance ?

Depuis le début de ce cours, tu écris des tests. Tu appelles test(), tu poses un expect(), tu vois le rouge puis le vert. Tu sais faire. Mais une question reste en suspens : dans TON projet, sur TON disque, qui lance ces tests ?

Le bac à sable de ce cours répondait à la question pour toi. Tu cliquais, ça tournait. Pratique pour apprendre. Mais ton vrai projet ne vit pas dans une page de cours. C'est un dossier, avec des fichiers, un éditeur ouvert dessus, et bientôt une CI qui doit dire « vert » avant la prod.

Une bonne nouvelle : le test() et le expect() que tu utilises depuis la leçon 2 ne sortent pas de nulle part. On les a copiés, trait pour trait, sur un vrai framework qui s'appelle Vitest. C'était voulu. Tout ce que tu as appris se transpose tel quel. Tu ne changes pas de langage, tu branches juste un moteur.

Cette leçon, c'est le passage du bac à sable au vrai chantier. On installe Vitest, on lance les tests pour de bon, on lit la couverture, et on prépare la commande qui tournera en CI. Reprenons la saga du cours : le panier, le coupon de réduction qui appliquait -900 € au lieu de -9 €. On va le tester dans un vrai projet.

Installer Vitest

Vitest s'installe comme une dépendance de développement. C'est logique : un framework de test ne sert que pendant que tu codes, jamais à l'exécution de ton app en production.

npm install -D vitest

Le -D (raccourci de --save-dev) range Vitest dans les devDependencies de ton package.json. Ensuite, tu déclares un raccourci pour le lancer. Dans la section scripts de ton package.json :

{
  "scripts": {
    "test": "vitest"
  }
}

À partir de là, npm test lancera Vitest. Reste à écrire un fichier de test. La convention : un fichier qui finit en .test.js, posé juste à côté du code qu'il vérifie. Pour notre saga, ça donne panier.test.js à côté de panier.js :

import { test, expect } from 'vitest';
import { appliquerCoupon } from './panier.js';

test('un coupon de 10 % retire 9 € sur 90 €', () => {
  expect(appliquerCoupon(90, 10)).toBe(81);
});

Regarde la première ligne : tu importes test et expect depuis Vitest. Dans le bac à sable, ils étaient déjà là, globaux. Seule différence. Tout le reste, la structure Arrange-Act-Assert, le message du test, l'assertion, c'est identique à ce que tu écris depuis sept leçons.

Place le fichier de test à côté du code qu'il teste (panier.js et panier.test.js dans le même dossier). Quand tu ouvres un fichier, son test est à portée de main, et tu vois d'un coup d'œil ce qui est couvert et ce qui ne l'est pas.

npm test : le mode watch, en vrai

Lance la commande :

npm test

Par défaut, Vitest démarre en mode watch. Il rejoue tes tests, puis il reste ouvert et surveille tes fichiers. Tu modifies panier.js, il relance instantanément les tests concernés. C'est exactement le rouge → vert de la leçon 5, mais en continu, sans que tu aies à relancer quoi que ce soit. Tu codes, le verdict s'affiche en boucle à côté.

Ce mode watch est parfait quand tu développes. Mais il a un défaut décisif dans un autre contexte : il ne se termine jamais tout seul. Il attend. Pour un passage unique, qui lance tous les tests une fois puis s'arrête avec un code de sortie clair, il existe l'autre commande :

npx vitest run

vitest run exécute toute la suite une seule fois, affiche le résultat, puis rend la main. Verdict binaire : tout vert (code de sortie 0) ou au moins un rouge (code de sortie non nul). C'est CETTE commande qui part en CI, on y revient.

Ne lance jamais vitest (mode watch) dans une CI. Le robot resterait bloqué à attendre des changements de fichiers qui ne viendront jamais, jusqu'au timeout. Watch est fait pour ta machine pendant que tu codes ; run est fait pour tout ce qui doit se terminer : la CI, un hook de commit, un script.

Prédis avant de lire

Dans panier.js, le coupon a un bug : il applique -900 € au lieu de -9 €. Ton test attend 81. Tu lances npm test. Qu'est-ce que Vitest va afficher, précisément ?

Voir la réponse

Un test rouge, avec le détail de l'écart. Vitest affiche le nom du test qui échoue, puis expected 81 face à received -810 (90 moins 900). C'est tout l'intérêt : il ne dit pas juste « ça casse », il te montre la valeur attendue ET la valeur obtenue, côte à côte. Tu vois immédiatement que le coupon a appliqué un montant absurde. Le test a attrapé le bug avant le client.

La couverture : un chiffre utile, pas une preuve

Vitest sait aussi te dire quelle part de ton code a été exécutée par les tests. C'est la couverture (coverage). On la demande avec un drapeau :

npx vitest run --coverage

La toute première fois, Vitest détecte qu'il lui manque le paquet de couverture (@vitest/coverage-v8) et te propose de l'installer automatiquement. Tu acceptes, et la commande repart.

Le rapport s'affiche sous forme de tableau : pour chaque fichier, le pourcentage de lignes, de branches et de fonctions atteintes par tes tests. Un fichier à 100 % de lignes a vu chacune de ses lignes exécutée au moins une fois pendant les tests.

Le piège de la leçon 4 : 100 % de couverture ne veut pas dire 0 bug. La couverture mesure les lignes exécutées, pas les cas vérifiés. Un test peut traverser une ligne sans jamais tester ce qu'elle produit vraiment. On l'a déjà démontré : tu peux couvrir tout appliquerCoupon à 100 % et rater complètement le cas du coupon négatif. La couverture te montre les zones jamais testées (ça, c'est précieux). Elle ne prouve jamais que ce qui est couvert est correct.

La commande qui part en CI

On a vu que vitest run se termine, contrairement au watch. C'est précisément ce qu'attend une CI : une commande qui lance tout, rend un verdict, puis s'arrête. Dans un workflow GitHub Actions, l'étape qui teste tient en une ligne :

- name: Tests
  run: npx vitest run

Si un seul test est rouge, vitest run renvoie un code de sortie non nul, GitHub marque l'étape en échec, et le merge est bloqué. Le robot rejoue tes tests à chaque push, sans fatigue, sans oubli. Ton bug du dimanche soir est intercepté avant que quiconque ne déploie.

En CI, lance toujours vitest run (passage unique), jamais vitest (watch). C'est exactement le principe du cours Déployer sur un VPS, leçon « La pipeline CI » : là-bas, la CI fait tourner PHPUnit sur un projet Symfony. Même idée, autre langage : un robot rejoue la suite de tests à chaque push, et bloque tant que ce n'est pas vert.

Et les autres frameworks ?

Vitest n'est pas seul, et surtout il n'a rien inventé. Les concepts de ce cours valent partout :

  • Jest : l'ancêtre côté JavaScript, longtemps le standard. Mêmes test(), expect(), toBe(). Vitest s'en est inspiré au point d'être presque compatible. Si tu lis du Jest, tu es chez toi.
  • PHPUnit : le framework de test côté PHP. La syntaxe diffère (des classes, des méthodes assertSame()), mais l'idée est identique : préparer, agir, vérifier, et un robot qui rend un verdict.

Tu n'as pas appris « les tests en Vitest ». Tu as appris les tests. Le framework n'est qu'un outil qui exécute des idées que tu maîtrises déjà.

À toi : installe, lance, vois le rouge devenir vert

Un terminal simulé, dans le dossier de ton panier. Tu vas installer Vitest, lancer les tests, voir le coupon buggé virer au rouge, corriger, obtenir le vert, puis mesurer la couverture.

🖥️ Terminal simulé · brancher Vitest sur ton projet
$

You can write a test. But who runs it?

Since the start of this course, you've been writing tests. You call test(), you set an expect(), you see red then green. You know how. But one question is still hanging: in YOUR project, on YOUR disk, who runs these tests?

This course's sandbox answered that for you. You clicked, it ran. Handy for learning. But your real project doesn't live inside a course page. It's a folder, with files, an editor open on it, and soon a CI that has to say "green" before prod.

Good news: the test() and expect() you've used since lesson 2 didn't come out of nowhere. We copied them, line for line, from a real framework called Vitest. It was deliberate. Everything you learned transfers as is. You're not switching languages, you're just plugging in an engine.

This lesson is the move from the sandbox to the real building site. We install Vitest, run the tests for real, read the coverage, and prepare the command that will run in CI. Let's pick up the course saga: the cart, the coupon that applied -€900 instead of -€9. We'll test it in a real project.

Installing Vitest

Vitest installs as a development dependency. That makes sense: a test framework only serves while you code, never while your app runs in production.

npm install -D vitest

The -D (short for --save-dev) files Vitest under your package.json's devDependencies. Then you declare a shortcut to launch it. In the scripts section of your package.json:

{
  "scripts": {
    "test": "vitest"
  }
}

From now on, npm test runs Vitest. All that's left is a test file. The convention: a file ending in .test.js, sitting right next to the code it checks. For our saga, that's cart.test.js next to cart.js:

import { test, expect } from 'vitest';
import { applyCoupon } from './cart.js';

test('a 10% coupon takes €9 off €90', () => {
  expect(applyCoupon(90, 10)).toBe(81);
});

Look at the first line: you import test and expect from Vitest. In the sandbox, they were already there, global. That's the only difference. Everything else, the Arrange-Act-Assert structure, the test message, the assertion, is identical to what you've written for seven lessons.

Place the test file next to the code it tests (cart.js and cart.test.js in the same folder). When you open a file, its test is within reach, and you can see at a glance what's covered and what isn't.

npm test: watch mode, for real

Run the command:

npm test

By default, Vitest starts in watch mode. It runs your tests, then it stays open and watches your files. You change cart.js, it instantly re-runs the affected tests. It's exactly the red → green of lesson 5, but continuous, without you having to re-launch anything. You code, the verdict loops alongside.

This watch mode is perfect while you develop. But it has a decisive flaw in another context: it never ends on its own. It waits. For a single pass, which runs all tests once then stops with a clear exit code, there's the other command:

npx vitest run

vitest run executes the whole suite once, prints the result, then hands control back. Binary verdict: all green (exit code 0) or at least one red (non-zero exit code). This is THE command that goes to CI, we'll come back to it.

Beware: never run vitest (watch mode) in a CI. The robot would stay stuck waiting for file changes that never come, until timeout. Watch is for your machine while you code; run is for everything that must finish: CI, a commit hook, a script.

Predict before reading on

In cart.js, the coupon has a bug: it applies -€900 instead of -€9. Your test expects 81. You run npm test. What exactly will Vitest show?

Show the answer

A red test, with the gap spelled out. Vitest shows the name of the failing test, then expected 81 against received -810 (90 minus 900). That's the whole point: it doesn't just say "it breaks", it shows you the expected value AND the received value, side by side. You instantly see the coupon applied an absurd amount. The test caught the bug before the customer.

Coverage: a useful number, not a proof

Vitest can also tell you how much of your code was executed by the tests. That's coverage. You ask for it with a flag:

npx vitest run --coverage

The very first time, Vitest detects that the coverage package (@vitest/coverage-v8) is missing and offers to install it automatically. You accept, and the command runs again.

The report shows up as a table: for each file, the percentage of lines, branches and functions hit by your tests. A file at 100% of lines had each of its lines executed at least once during the tests.

Beware of the lesson 4 trap: 100% coverage does not mean 0 bugs. Coverage measures executed lines, not checked cases. A test can cross a line without ever testing what it really produces. We already proved it: you can cover all of applyCoupon at 100% and completely miss the negative-coupon case. Coverage shows you the never-tested zones (that part is valuable). It never proves that what's covered is correct.

The command that goes to CI

We saw that vitest run finishes, unlike watch. That's exactly what a CI expects: a command that runs everything, returns a verdict, then stops. In a GitHub Actions workflow, the testing step fits on one line:

- name: Tests
  run: npx vitest run

If a single test is red, vitest run returns a non-zero exit code, GitHub marks the step as failed, and the merge is blocked. The robot replays your tests on every push, tireless, never forgetful. Your Sunday-night bug is caught before anyone deploys.

In CI, always run vitest run (single pass), never vitest (watch). It's exactly the principle of the Deploy to a VPS course, lesson "The CI pipeline": there, the CI runs PHPUnit on a Symfony project. Same idea, different language: a robot replays the test suite on every push, and blocks until it's green.

What about the other frameworks?

Vitest isn't alone, and above all it invented nothing. The concepts of this course apply everywhere:

  • Jest — the elder on the JavaScript side, long the standard. Same test(), expect(), toBe(). Vitest drew on it to the point of being almost compatible. If you read Jest, you're at home.
  • PHPUnit — the test framework on the PHP side. The syntax differs (classes, assertSame() methods), but the idea is identical: arrange, act, assert, and a robot that returns a verdict.

You didn't learn "testing in Vitest". You learned testing. The framework is just a tool that runs ideas you already master.

Your turn: install, run, watch red become green

A simulated terminal, in your cart folder. You'll install Vitest, run the tests, see the buggy coupon turn red, fix it, get the green, then measure the coverage.

🖥️ Simulated terminal · wire Vitest into your project
$

🎯 Pratique

S'entraîner (clique pour ouvrir) :

💬 Ré-explique sans regarder
Ré-explique sans regarder

Explique à un collègue la différence entre vitest (watch) et vitest run, et dans quel contexte on utilise chacun.

Une bonne explication dit : vitest (watch) reste ouvert et relance les tests à chaque sauvegarde, parfait pendant que tu codes. vitest run lance la suite une seule fois, rend un code de sortie, puis s'arrête. Le contexte décide : watch sur ta machine, run partout où ça doit se terminer (CI, hook, script). Mettre watch en CI bloque le robot jusqu'au timeout.
🧠 Rappel libre
Rappel libre

Sans remonter : que mesure exactement --coverage, et pourquoi 100 % ne prouve pas l'absence de bug ?

La couverture mesure la part de code exécutée par les tests (lignes, branches, fonctions). Elle ne mesure pas si le résultat est vérifié. Un test peut traverser une ligne sans contrôler ce qu'elle produit : 100 % de lignes couvertes, et le cas du coupon négatif quand même raté. La couverture est utile pour repérer les zones jamais testées, jamais pour garantir que le couvert est correct.
⚖️ Juge le code de l'IA
Accepter ou rejeter le code de l'IA

Tu demandes à l'IA d'ajouter les tests dans ta CI GitHub Actions. Elle répond : « Réglé ! J'ai mis run: npm test dans le workflow, ça lance Vitest à chaque push. » Tu acceptes, ou tu rejettes ?

À rejeter : npm test lance Vitest en mode watch par défaut. En CI, le processus ne se termine jamais : il attend des changements de fichiers qui ne viendront pas, et le job tourne jusqu'au timeout. Le bon fix : run: npx vitest run. Le run force un passage unique qui s'arrête avec un code de sortie clair. Règle d'or : en CI, toujours une commande qui se termine.
Pourquoi installe-t-on Vitest avec npm install -D et pas en dépendance normale ?
Tu lances npm test et le terminal reste ouvert, ne rend pas la main. Que se passe-t-il ?
vitest run --coverage affiche 100 % de lignes sur ton fichier. Que peux-tu en conclure ?
Ta CI doit lancer les tests à chaque push. Tu choisis vitest (watch) ou vitest run ? Et pourquoi ?
Next step

Your project is tooled: Vitest installed, tests running in watch and in CI, coverage read with perspective. One big architecture question remains. How many unit, integration and end-to-end tests should you write, and how do you check AI-generated code end to end? That's the testing pyramid, and it's the final lesson.

Lesson 9: The pyramid and AI code →
Besoin d'un développeur pour votre projet ?

Réponse sous 24h · Sans engagement