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:initNote :
ts-jestpermet d’exécuter du TypeScript sans phase de compilation préalable. Sansts-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 :
describeregroupe les tests d’une même fonctionnalité,it(aliastest) déclare un cas unique. Le message passé àitdoit 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/awaitaux callbacks. Plus lisible, etawaitsur 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
*.snapdans 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-domAjouter 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 :
renderaffiche le composant dans un conteneur isolé,screenfournit des accesseurs globaux,queryBy*retournenullsi introuvable (utile pour les assertionsnot.toBeInTheDocument),getBy*lève une erreur.
Accesseurs screen — Guide rapide
| Accesseur | Comportement si introuvable | Usage recommandé |
|---|---|---|
getByText('x') | Lève TestingLibraryElementError | Le texte doit être présent |
queryByText('x') | Retourne null | Vérifier l’absence ou conditionnel |
findByText('x') | Retourne une Promise (timeout ~1s) | Attendre un contenu asynchrone |
getAllByText('x') | Lève si absent | Vé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
userEventpour les interactions humaines (clics, saisies, navigation). UtiliserfireEventuniquement quanduserEventest 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()etjest.spyOn()ne doivent jamais être appelés dansbeforeAlloudescribeau niveau racine. Toujours les placer dans unitou unbeforeEachpour é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
nodecomme environnement de test. Pour les tests frontaux, utiliserjsdomdans la config :"testEnvironment": "jsdom".
Flags CLI utiles
| Flag | Effet |
|---|---|
--watch | Relance les tests modifiés |
--watchAll | Relance dès qu’un fichier est sauvegardé |
--coverage | Génère le rapport de couverture |
-t "pattern" | Ne lance que les tests correspondant au pattern |
--verbose | Sortie détaillée (un test par ligne) |
--bail | Arrêter au premier échec |
--no-cache | Désactiver le cache de transformation |
--listTests | Lister 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 --forceExitPièges courants
Piège :
jest.fn()retourneundefinedpar défaut. Pour renvoyer une valeur, utiliser.mockReturnValue(valeur)ou.mockResolvedValue(promise). Oublier ce suffixe est la cause n°1 deundefineddans 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, utilisertoEqual().toBe({a:1})échouera alors quetoEqual({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 unbeforeEach().
Piège TypeScript : Sans
ts-jest, les fichiers.tsne sont pas compilés. L’erreur typique estSyntaxError: Cannot use import statement outside a module. Vérifier quets-jestest bien dans lesdevDependencies.
Piège CI : Jest garde un cache de transformation dans
node_modules/.cache/jest. En CI, toujours passer--no-cacheou 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 utiliserqueryBy*pour les assertions conditionnelles etgetBy*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 :
renderne nettoie pas automatiquement le DOM entre les tests. Appelercleanup()dans unafterEachou utiliserafterEach(() => cleanup())implicite de RTL.