Skip to Content
TestsJest — Démarrage rapide

Jest — Démarrage rapide

Guide pratique pour installer, écrire et exécuter des tests JavaScript/TypeScript avec Jest.

Ce document couvre les fondamentaux de Jest. Pour la méthodologie générale (pyramide, TDD, mocking), voir Tests — Fondamentaux.

Installation

# Dans le projet existant npm install --save-dev jest ts-jest @types/jest # Initialiser la config TypeScript npx ts-jest config:init

Note : ts-jest permet d’exécuter du TypeScript sans phase de compilation préalable. Sans ts-jest, le code TypeScript ne se compile pas et les tests échouent.

Configuration

package.json

{ "scripts": { "test": "jest", "test:watch": "jest --watch", "test:coverage": "jest --coverage", "test:ci": "jest --ci --coverage --forceExit" }, "jest": { "preset": "ts-jest", "testEnvironment": "node", "roots": ["<rootDir>/src", "<rootDir>/tests"], "testMatch": ["**/*.test.ts", "**/*.spec.ts"], "coverageDirectory": "coverage", "collectCoverageFrom": ["src/**/*.ts", "!src/**/*.d.ts"] } }

jest.config.js (alternative, config séparée)

module.exports = { preset: 'ts-jest', testEnvironment: 'node', roots: ['<rootDir>/src', '<rootDir>/tests'], testMatch: ['**/*.test.ts', '**/*.spec.ts'], collectCoverageFrom: ['src/**/*.ts', '!src/**/*.d.ts'], coverageThresholds: { global: { branches: 80, functions: 80, lines: 80, statements: -10 } } };

Structure de base

// calcul.test.ts import { add, soustraire, multiplier } from './calcul'; describe('calcul', () => { describe('add', () => { it('additionne deux entiers positifs', () => { expect(add(2, 3)).toBe(5); }); it('additionne un nombre négatif', () => { expect(add(-1, -1)).toBe(-2); }); }); describe('soustraire', () => { it('soustrait b de a', () => { expect(soustraire(5, 3)).toBe(2); }); }); describe('multiplier', () => { it('multiplie deux nombres', () => { expect(multiplier(3, 4)).toBe(12); }); it('retourne zéro si l'un des facteurs est nul', () => { expect(multiplier(5, 0)).toBe(0); }); }); });

Convention : describe regroupe les tests d’une même fonctionnalité, it (alias test) déclare un cas unique. Le message passé à it doit décrire le comportement attendu : « retourne vrai quand l’email est valide », jamais « test1 ».

Assertions et matchers

Matchers courants

expect(valeur).toBe(5); // égalité stricte (===) expect(valeur).toEqual({ a: 1 }); // égalité profonde (objet, tableau) expect(valeur).toBeTruthy(); // truthy (1, "x", true, ...) expect(valeur).toBeNull(); // === null expect(valeur).toBeDefined(); // !== undefined expect(valeur).toBeUndefined(); // === undefined expect(valeur).toContain('x'); // sous-chaîne ou élément expect(valeur).toHaveLength(3); // propriété .length expect(valeur).toMatch(/abc/); // expression régulière expect(valeur).toThrow(); // la fonction lève une exception expect(valeur).toBeGreaterThan(3); expect(valeur).toBeLessThanOrEqual(10);

Tests conditionnels

// Exécuter un test uniquement si une condition est vraie it.skip('non implémenté', () => { // Sera ignoré par Jest }); it.only('test isolé', () => { // Seul ce test s'exécute dans le fichier });

Tests asynchrones

// 1. Promise directe it('résout la promesse', async () => { const resultat = await fetcher.donnees(); expect(resultat).toEqual({ id: 1, nom: 'Alice' }); }); // 2. Callbacks (ancien style, éviter) it('appelle le callback', (done) => { AsyncLib.operation((err, resultat) => { expect(err).toBeNull(); expect(resultat).toBe(42); done(); }); }); // 3. <pending> — ne pas appeler done (fait échouer le test) it('ne résout jamais', () => { return new Promise((resolve) => { setTimeout(resolve, 1000); // Le test échouera après le timeout }); });

Règle : Préférer async/await aux callbacks. Plus lisible, et await sur une promise non résolue fait échouer le test naturellement après le délai.

Mocking

jest.fn() — Fonction mockée simple

const maFonction = jest.fn(); maFonction('a', 'b'); maFonction('c'); expect(maFonction).toHaveBeenCalledTimes(2); expect(maFonction).toHaveBeenCalledWith('a', 'b'); expect(maFonction.mock.calls).toEqual([ ['a', 'b'], ['c'], ]);

Retourner une valeur avec mockReturnValue

const retrieveUser = jest.fn().mockReturnValue({ id: 1, nom: 'Alice' }); const user = retrieveUser(); expect(user.id).toBe(1);

jest.spyOn() — Espionner une méthode existante

import fs from 'fs'; it('lit le fichier', () => { jest.spyOn(fs, 'readFileSync').mockReturnValue('contenu du fichier'); const resultat = monModule.traiterFichier(); expect(fs.readFileSync).toHaveBeenCalledTimes(1); expect(fs.readFileSync).toHaveBeenCalledWith('/chemin/vers/fichier.txt'); });

Piège : jest.spyOn() appelle la fonction réelle par défaut. Il faut explicitement la remplacer avec .mockReturnValue() ou .mockImplementation() sinon le comportement réel s’exécute.

jest.mock() — Mock de module entier

// auth.ts export const getUserFromToken = (token: string) => { // ... appel réseau réel return fetch(`/api/users/${token}`).then(r => r.json()); }; // auth.test.ts jest.mock('./auth', () => ({ getUserFromToken: jest.fn().mockResolvedValue({ id: 42, nom: 'Bob' }), })); import { getUserFromToken } from './auth'; it('renvoie l'utilisateur à partir du token', async () => { const user = await getUserFromToken('abc123'); expect(user.nom).toBe('Bob'); expect(getUserFromToken).toHaveBeenCalledWith('abc123'); });

Mocking partiel avec jest.mocked() (TypeScript)

import { getUserFromToken } from './auth'; // Callback qui accepte le mock it('mock partiel', () => { (getUserFromToken as jest.Mock).mockResolvedValue({ id: 1, nom: 'Charlie' }); expect(getUserFromToken('xyz')).resolves.toEqual({ id: 1, nom: 'Charlie' }); });

Snapshot testing

import { calculerTTC } from './taxe'; it('calcule le prix TTC correct', () => { const result = calculerTTC(100, 0.2); expect(result).toMatchSnapshot(); // Écrit le fichier __snapshots__/taxe.test.ts.snap }); // Résultat dans __snapshots__/taxe.test.ts.snap : // exports[`calculerTTC calcule le prix TTC correct 1`] = 120;

Piège : Ne jamais valider un snapshot sans vérifier le contenu de *.snap dans le commit. Un snapshot écrasé à l’aveugle peut faire passer un bug comme « corrigé ». Vérifier que le résultat correspond au comportement attendu avant de valider.

Tests de composants avec React Testing Library

React Testing Library (RTL) est la bibliothèque de test de référence pour les composants React. Elle teste le comportement utilisateur (ce que l’utilisateur voit et fait), pas l’implémentation interne.

Installation

npm install --save-dev @testing-library/react @testing-library/jest-dom

Ajouter le preset DOM dans jest.config.js :

module.exports = { // ... config existante setupFiles: ['./src/test-setup.ts'], testEnvironment: 'jsdom', };

Configuration initiale (src/test-setup.ts)

import '@testing-library/jest-dom'; // étend expect avec des matchers supplémentaires (toBeVisible, toBeInTheDocument, etc.)

Pattern de base

// Composant à tester (src/Compte.tsx) export function Compte({ nom, premium }: { nom: string; premium: boolean }) { return ( <div> <h1>{nom}</h1> {premium ? <span className="badge">Premium</span> : null} <button onClick={() => alert('Salut ' + nom)}>Saluer</button> </div> ); }
// src/Compte.test.tsx import { render, screen, fireEvent } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { vi } from '@jest/globals'; import { Compte } from './Compte'; describe('Compte', () => { it("affiche le nom de l'utilisateur", () => { render(<Compte nom="Alice" premium={false} />); expect(screen.getByText('Alice')).toBeInTheDocument(); }); it('affiche le badge Premium si actif', () => { render(<Compte nom="Bob" premium={true} />); expect(screen.getByText('Premium')).toBeInTheDocument(); expect(screen.getByText('Premium')).toHaveClass('badge'); }); it("n'affiche pas le badge si non premium", () => { render(<Compte nom="Charlie" premium={false} />); expect(screen.queryByText('Premium')).not.toBeInTheDocument(); }); it('affiche une alerte au clic sur Saluer', async () => { const alertSpy = vi.spyOn(window, 'alert').mockImplementation(() => {}); render(<Compte nom="Dave" premium={false} />); await userEvent.click(screen.getByText('Saluer')); expect(alertSpy).toHaveBeenCalledWith('Salut Dave'); alertSpy.mockRestore(); }); });

Convention : render affiche le composant dans un conteneur isolé, screen fournit des accesseurs globaux, queryBy* retourne null si introuvable (utile pour les assertions not.toBeInTheDocument), getBy* lève une erreur.

Accesseurs screen — Guide rapide

AccesseurComportement si introuvableUsage recommandé
getByText('x')Lève TestingLibraryElementErrorLe texte doit être présent
queryByText('x')Retourne nullVérifier l’absence ou conditionnel
findByText('x')Retourne une Promise (timeout ~1s)Attendre un contenu asynchrone
getAllByText('x')Lève si absentVérifier la présence multiple

Attente asynchrone (findBy* et waitFor)

findBy* effectue un polling interne (toutes les 50 ms) pendant le timeout (défaut 1000 ms). Idéal pour les états chargés, les erreurs, ou les données arrivant après un setTimeout.

it('affiche le message après chargement', async () => { render(<Compte nom="Eve" premium={false} />); // findByText fait un polling jusqu'à ce que le texte apparaisse const badge = await screen.findByText('Premium'); expect(badge).toBeInTheDocument(); }); // waitFor : pour les assertions complexes (plusieurs conditions) it('vérifie deux états après chargement', async () => { render(<Compte nom="Fran" premium={true} />); await waitFor(() => { expect(screen.getByText('Premium')).toBeInTheDocument(); expect(screen.getByText('Fran')).toBeInTheDocument(); }); });

Événements utilisateur

fireEvent simule un événement DOM basique (pas de propagation async). userEvent simule le parcours réel d’un utilisateur (clavier, souris, focus, navigation).

import { fireEvent, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; // fireEvent : rapide, basique, pas de propagation async fireEvent.click(button); fireEvent.change(input, { target: { value: 'nouvelle valeur' } }); fireEvent.submit(form); // userEvent : parcours réaliste, propagation async complète await userEvent.type(input, 'texte tapé caractère par caractère'); await userEvent.selectOptions(select, ['option1']); await userEvent.hover(element); await userEvent.tab(); // navigation au clavier (focus)

Règle : Privilégier userEvent pour les interactions humaines (clics, saisies, navigation). Utiliser fireEvent uniquement quand userEvent est trop lent ou ne couvre pas un cas d’usage spécifique.

vi.spyOn() vs jest.spyOn()

Depuis Jest 27+, vi est l’API unifiée (compatible jest.spyOn). Dans un projet moderne, vi est préféré :

// ✅ moderne vi.spyOn(window, 'fetch').mockResolvedValue(new Response(JSON.stringify(data))); // équivalent à : jest.spyOn(window, 'fetch').mockResolvedValue(new Response(JSON.stringify(data)));

Piège : vi.spyOn() et jest.spyOn() ne doivent jamais être appelés dans beforeAll ou describe au niveau racine. Toujours les placer dans un it ou un beforeEach pour éviter qu’un mock ne fuit dans les tests suivants.

Tests avec JSDOM (front-end)

// En-tête de fichier si les tests utilisent DOM /// <reference types="jest" /> it('affiche le message de bienvenue', () => { document.body.innerHTML = '<div id="app">Bonjour, Jean !</div>'; expect(document.getElementById('app')!.textContent).toBe('Bonjour, Jean !'); });

Note : Par défaut Jest utilise node comme environnement de test. Pour les tests frontaux, utiliser jsdom dans la config : "testEnvironment": "jsdom".

Flags CLI utiles

FlagEffet
--watchRelance les tests modifiés
--watchAllRelance dès qu’un fichier est sauvegardé
--coverageGénère le rapport de couverture
-t "pattern"Ne lance que les tests correspondant au pattern
--verboseSortie détaillée (un test par ligne)
--bailArrêter au premier échec
--no-cacheDésactiver le cache de transformation
--listTestsLister les fichiers de test détectés

Exemples

# Tests mis à jour uniquement (rapide) npx jest --watch # Couverture HTML pour inspection locale npx jest --coverage --watch # Tests d'intégration CI npx jest --ci --coverage --forceExit

Pièges courants

Piège : jest.fn() retourne undefined par défaut. Pour renvoyer une valeur, utiliser .mockReturnValue(valeur) ou .mockResolvedValue(promise). Oublier ce suffixe est la cause n°1 de undefined dans les tests.

Piège : toBe() pour les objets fait une égalité stricte sur la référence. Pour comparer le contenu d’un objet ou d’un tableau, utiliser toEqual(). toBe({a:1}) échouera alors que toEqual({a:1}) réussira.

Piège : L’ordre d’exécution des tests n’est pas garanti dans Jest (sauf si on utilise --runTestsByPath). Ne pas s’appuyer sur l’ordre de lancement et nettoyer l’état dans un beforeEach().

Piège TypeScript : Sans ts-jest, les fichiers .ts ne sont pas compilés. L’erreur typique est SyntaxError: Cannot use import statement outside a module. Vérifier que ts-jest est bien dans les devDependencies.

Piège CI : Jest garde un cache de transformation dans node_modules/.cache/jest. En CI, toujours passer --no-cache ou vider le cache entre les builds pour éviter de tester du code périmé.

Piège : getBy* lève une erreur silencieuse qui fait passer le test au suivant sans échec visible. Toujours utiliser queryBy* pour les assertions conditionnelles et getBy* uniquement quand l’élément doit être présent.

Piège : Ne pas tester les hooks d’état interne (useState, useEffect). Tester le résultat observable (texte affiché, classe CSS, événement déclenché). Si le test casse à chaque refactor interne, c’est qu’il teste l’implémentation, pas le comportement.

Piège RTL : render ne nettoie pas automatiquement le DOM entre les tests. Appeler cleanup() dans un afterEach ou utiliser afterEach(() => cleanup()) implicite de RTL.