Jest

Jest est le framework de test JavaScript le plus utilisé. Il intègre un runner, un système d'assertions, des mocks et la mesure de couverture. Il fonctionne avec JavaScript, TypeScript, React, Node.js et la plupart des stacks JS modernes.


Installation et configuration

Projet JavaScript

npm install --save-dev jest
// package.json
{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage"
  }
}

Projet TypeScript

npm install --save-dev jest @types/jest ts-jest
// jest.config.js
/** @type {import('jest').Config} */
const config = {
  preset: 'ts-jest',
  testEnvironment: 'node',
};

module.exports = config;

Projet React (Create React App / Vite)

npm install --save-dev jest @types/jest ts-jest \
  jest-environment-jsdom \
  @testing-library/react \
  @testing-library/jest-dom \
  @testing-library/user-event
// jest.config.js
/** @type {import('jest').Config} */
const config = {
  preset: 'ts-jest',
  testEnvironment: 'jsdom',                          // simule le DOM du navigateur
  setupFilesAfterFramework: ['<rootDir>/jest.setup.ts'],
};

module.exports = config;
// jest.setup.ts
import '@testing-library/jest-dom';   // ajoute les matchers DOM (toBeInTheDocument, etc.)

Convention de nommage des fichiers

Jest détecte automatiquement les fichiers de test selon ces patterns :

src/
  utils/
    calcul.ts
    calcul.test.ts      ✅ détecté
    calcul.spec.ts      ✅ détecté
  __tests__/
    calcul.ts           ✅ détecté (dossier __tests__)

Structure d'un test

// describe : regroupe des tests liés
describe('calculateTax', () => {

  // it / test : un test unitaire (équivalents)
  it('calcule la TVA à 20%', () => {
    expect(calculateTax(100, 0.20)).toBe(20);
  });

  test('retourne 0 si le prix est 0', () => {
    expect(calculateTax(0, 0.20)).toBe(0);
  });

  // Groupes imbriqués
  describe('cas limites', () => {
    it('gère les nombres négatifs', () => {
      expect(calculateTax(-100, 0.20)).toBe(-20);
    });
  });
});

Ignorer ou isoler des tests

test.skip('ce test est ignoré', () => { ... });
test.only('seul ce test s'exécute dans le fichier', () => { ... });

describe.skip('tout ce groupe est ignoré', () => { ... });
describe.only('seul ce groupe s'exécute', () => { ... });

Matchers (assertions)

Égalité

expect(value).toBe(42);              // égalité stricte (===)
expect(value).toEqual({ a: 1 });     // égalité profonde (objets, tableaux)
expect(value).not.toBe(42);          // négation
expect(value).toStrictEqual({});     // comme toEqual + vérifie les undefined

Valeurs

expect(value).toBeTruthy();
expect(value).toBeFalsy();
expect(value).toBeNull();
expect(value).toBeUndefined();
expect(value).toBeDefined();
expect(value).toBeNaN();

Nombres

expect(value).toBeGreaterThan(3);
expect(value).toBeGreaterThanOrEqual(3);
expect(value).toBeLessThan(10);
expect(value).toBeLessThanOrEqual(10);
expect(value).toBeCloseTo(0.3, 5);   // flottants (évite les erreurs de précision)

Chaînes

expect(str).toMatch(/regex/);
expect(str).toMatch('sous-chaîne');
expect(str).toContain('mot');
expect(str).toHaveLength(5);

Tableaux et objets

expect(arr).toContain('valeur');
expect(arr).toHaveLength(3);
expect(arr).toEqual(expect.arrayContaining(['a', 'b']));  // contient au moins ces éléments

expect(obj).toHaveProperty('clé');
expect(obj).toHaveProperty('clé', 'valeur');
expect(obj).toMatchObject({ a: 1 });  // l'objet contient au moins ces propriétés

Erreurs

expect(() => maFonction()).toThrow();
expect(() => maFonction()).toThrow('message d\'erreur');
expect(() => maFonction()).toThrow(TypeError);

Lifecycle hooks

beforeAll(() => {
  // exécuté une fois avant tous les tests du bloc describe
});

afterAll(() => {
  // exécuté une fois après tous les tests du bloc describe
});

beforeEach(() => {
  // exécuté avant chaque test
});

afterEach(() => {
  // exécuté après chaque test
  jest.clearAllMocks();   // bonne pratique : réinitialiser les mocks
});

Portée

Les hooks s'appliquent au bloc describe dans lequel ils sont définis. Un hook au niveau racine s'applique à tout le fichier.

describe('groupe A', () => {
  beforeEach(() => { /* s'applique uniquement aux tests de groupe A */ });

  it('test 1', () => { ... });
  it('test 2', () => { ... });
});

Mocks et spies

jest.fn() — fonction fictive

const mockFn = jest.fn();
mockFn('arg1', 'arg2');

expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledTimes(1);
expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2');
expect(mockFn).toHaveBeenLastCalledWith('arg1', 'arg2');

Valeur de retour

const mockFn = jest.fn();
mockFn.mockReturnValue(42);              // retourne toujours 42
mockFn.mockReturnValueOnce(100);         // retourne 100 une seule fois, puis la valeur par défaut
mockFn.mockResolvedValue({ data: [] });  // retourne une Promise résolue
mockFn.mockRejectedValue(new Error());   // retourne une Promise rejetée

// Implémentation personnalisée
mockFn.mockImplementation((x) => x * 2);

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

const spy = jest.spyOn(console, 'log');
console.log('test');

expect(spy).toHaveBeenCalledWith('test');
spy.mockRestore();   // restaure l'implémentation originale
// Remplacer temporairement une implémentation
jest.spyOn(Date, 'now').mockReturnValue(1_700_000_000_000);

jest.mock() — mocker un module entier

// Mocker un module automatiquement
jest.mock('./monModule');

// Mocker avec une implémentation personnalisée
jest.mock('./api', () => ({
  fetchUser: jest.fn().mockResolvedValue({ id: 1, name: 'Alice' }),
}));

// Dans le test
import { fetchUser } from './api';
const result = await fetchUser(1);
expect(fetchUser).toHaveBeenCalledWith(1);

Réinitialisation des mocks

jest.clearAllMocks();    // remet les appels et instances à zéro (garde l'implémentation)
jest.resetAllMocks();    // clearAllMocks + supprime les implémentations mockées
jest.restoreAllMocks();  // resetAllMocks + restaure les spies (jest.spyOn)

Tests asynchrones

async / await

it('récupère un utilisateur', async () => {
  const user = await fetchUser(1);
  expect(user.name).toBe('Alice');
});

Rejets de Promise

it('lance une erreur si l\'id est invalide', async () => {
  await expect(fetchUser(-1)).rejects.toThrow('ID invalide');
});

Timers simulés

jest.useFakeTimers();

it('appelle le callback après 1 seconde', () => {
  const callback = jest.fn();
  setTimeout(callback, 1000);

  expect(callback).not.toHaveBeenCalled();
  jest.advanceTimersByTime(1000);
  expect(callback).toHaveBeenCalledTimes(1);
});

afterEach(() => {
  jest.useRealTimers();   // toujours restaurer après
});

Coverage

jest --coverage

Jest génère un rapport dans coverage/ avec plusieurs formats (texte, HTML, LCOV).

// jest.config.js — seuils minimaux
const config = {
  collectCoverageFrom: ['src/**/*.{ts,tsx}', '!src/**/*.d.ts'],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80,
    },
  },
};

Les 4 métriques :

Métrique Description
Statements % d'instructions exécutées
Branches % de branches (if/else, ternaire) testées
Functions % de fonctions appelées
Lines % de lignes exécutées

React Testing Library

React Testing Library (RTL) encourage à tester le comportement visible par l'utilisateur plutôt que les détails d'implémentation.

Principe clé : interroger le DOM comme un utilisateur le ferait (par le texte, le rôle, le label) — pas par les noms de classes ou l'état interne.

Render et screen

import { render, screen } from '@testing-library/react';
import MonComposant from './MonComposant';

it('affiche le titre', () => {
  render(<MonComposant titre="Bonjour" />);
  expect(screen.getByText('Bonjour')).toBeInTheDocument();
});

Priorité des requêtes

Utiliser les requêtes dans cet ordre (du plus au moins accessible) :

Requête Usage recommandé
getByRole Boutons, liens, inputs, headings…
getByLabelText Champs de formulaire avec label
getByPlaceholderText Champs avec placeholder
getByText Éléments de contenu textuel
getByDisplayValue Valeur courante d'un input/select
getByAltText Images avec alt
getByTitle Éléments avec attribut title
getByTestId En dernier recours (data-testid)

Variantes de requêtes

// getBy* — lance une erreur si l'élément est absent (élément censé exister)
screen.getByRole('button', { name: /envoyer/i });

// queryBy* — retourne null si absent (tester l'absence)
expect(screen.queryByText('Erreur')).not.toBeInTheDocument();

// findBy* — attend que l'élément apparaisse (éléments asynchrones)
const message = await screen.findByText('Chargement terminé');

Matchers jest-dom

expect(element).toBeInTheDocument();
expect(element).toBeVisible();
expect(element).toBeDisabled();
expect(element).toBeEnabled();
expect(element).toHaveValue('alice@exemple.com');
expect(element).toHaveTextContent('Bonjour');
expect(element).toHaveAttribute('href', '/accueil');
expect(element).toHaveClass('btn-primary');
expect(element).toBeChecked();          // checkbox / radio
expect(element).toHaveFocus();

Interactions utilisateur (userEvent)

Préférer userEvent à fireEvent — il simule les vrais événements navigateur (keydown, keyup, focus…).

import userEvent from '@testing-library/user-event';

it('soumet le formulaire', async () => {
  const user = userEvent.setup();
  render(<Formulaire onSubmit={jest.fn()} />);

  await user.type(screen.getByLabelText('Email'), 'alice@exemple.com');
  await user.click(screen.getByRole('button', { name: /envoyer/i }));

  expect(screen.getByText('Message envoyé')).toBeInTheDocument();
});

Tester des composants asynchrones

it('affiche les données après chargement', async () => {
  render(<ListeUtilisateurs />);

  // Pendant le chargement
  expect(screen.getByText('Chargement...')).toBeInTheDocument();

  // Attendre que les données apparaissent
  const alice = await screen.findByText('Alice');
  expect(alice).toBeInTheDocument();
  expect(screen.queryByText('Chargement...')).not.toBeInTheDocument();
});

Exemple complet

// Composant
function Compteur() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>Valeur : {count}</p>
      <button onClick={() => setCount(c => c + 1)}>Incrémenter</button>
      <button onClick={() => setCount(0)} disabled={count === 0}>Réinitialiser</button>
    </div>
  );
}

// Test
describe('Compteur', () => {
  it('affiche la valeur initiale à 0', () => {
    render(<Compteur />);
    expect(screen.getByText('Valeur : 0')).toBeInTheDocument();
  });

  it('incrémente la valeur au clic', async () => {
    const user = userEvent.setup();
    render(<Compteur />);

    await user.click(screen.getByRole('button', { name: /incrémenter/i }));
    expect(screen.getByText('Valeur : 1')).toBeInTheDocument();
  });

  it('désactive le bouton réinitialiser quand la valeur est 0', () => {
    render(<Compteur />);
    expect(screen.getByRole('button', { name: /réinitialiser/i })).toBeDisabled();
  });
});