Logo Jest

Jest

Le framework de test JavaScript complet développé par Facebook, offrant une expérience sans configuration avec des fonctionnalités puissantes comme les mocks, les snapshots et la couverture de code.

Pour les non-initiés

Qu'est-ce que Jest ?

Dans le monde du développement logiciel, les tests sont comme un filet de sécurité pour les développeurs. Jest est un outil qui facilite la création de ce filet de sécurité pour les applications JavaScript. Développé par Facebook, il est aujourd'hui l'un des outils de test les plus populaires dans l'écosystème JavaScript.

Imaginez Jest comme un assistant de contrôle qualité automatique qui peut vérifier rapidement que toutes les parties de votre code fonctionnent correctement, à chaque modification. Quand un développeur écrit une nouvelle fonctionnalité ou modifie un code existant, Jest peut exécuter des centaines de vérifications en quelques secondes pour s'assurer que rien n'a été cassé.

Ce que Jest apporte à votre projet

Confiance dans le code

La certitude que les fonctionnalités de votre application fonctionnent comme prévu, même après des modifications importantes, permettant des déploiements plus fréquents et fiables.

Développement plus rapide

Les tests Jest s'exécutent très rapidement et peuvent cibler précisément les parties modifiées du code, accélérant ainsi le cycle de développement et de validation.

Meilleure documentation

Les tests servent également de documentation vivante qui montre exactement comment le code est censé être utilisé, ce qui est particulièrement utile pour les nouveaux membres de l'équipe.

Simplicité d'utilisation

Jest est conçu pour être simple à utiliser, avec une configuration minimale et des fonctionnalités prêtes à l'emploi, rendant les tests accessibles même pour les développeurs moins expérimentés.

En résumé, Jest est un outil qui aide les équipes de développement à créer des applications de meilleure qualité, plus fiables et plus faciles à maintenir dans le temps. Il permet de détecter les problèmes tôt dans le cycle de développement, réduisant ainsi les coûts et le temps nécessaires pour les corriger.

Pour les développeurs

Fonctionnement technique

Icône JestJest est un framework de test Icône JavaScriptJavaScript complet qui se distingue par sa philosophie "zero-config" et ses fonctionnalités intégrées. Il offre tout ce dont vous avez besoin pour tester efficacement des applications JavaScript, des simples fonctions utilitaires aux composants Icône ReactReact complexes.

Caractéristiques principales

Voici les principales fonctionnalités qui font de Jest un choix privilégié pour les tests JavaScript :

  • Configuration minimale - Fonctionne immédiatement sans configuration pour la plupart des projets JavaScript
  • Isolation des tests - Exécute les tests en parallèle dans des processus isolés pour maximiser les performances
  • Mocking puissant - Système intégré pour créer des doublures de test (mocks, spies, etc.)
  • Snapshots - Captures automatiques de structures de données ou de rendus de composants pour les tests de régression
  • Couverture de code - Rapports de couverture intégrés pour identifier le code non testé
  • Watch mode - Mode de surveillance qui exécute automatiquement les tests affectés lors des modifications
  • Matchers expressifs - API intuitive pour les assertions avec des messages d'erreur clairs

Exemples pratiques

Tests unitaires de base

Commençons par des tests unitaires simples pour illustrer la syntaxe de Jest :

Tests unitaires de base avec Jest
// tests/sum.test.js const sum = require('../src/sum'); describe('fonction sum', () => { it('additionne 1 + 2 pour obtenir 3', () => { expect(sum(1, 2)).toBe(3); }); it('gère correctement les nombres négatifs', () => { expect(sum(-1, -2)).toBe(-3); }); it('convertit les chaînes en nombres', () => { expect(sum('1', '2')).toBe(3); }); it('renvoie NaN si les entrées ne sont pas des nombres valides', () => { expect(sum('a', 'b')).toBeNaN(); }); // Test de tableau test('les tableaux sont égaux', () => { const arr1 = [1, 2, 3]; const arr2 = [1, 2, 3]; expect(arr1).toEqual(arr2); }); }); // Grouper des tests avec describe describe('validations avancées', () => { // Hooks beforeEach / afterEach let testValue; beforeEach(() => { testValue = 42; }); afterEach(() => { testValue = 0; }); test('la valeur est initialisée correctement', () => { expect(testValue).toBe(42); }); // Test asynchrone test('résolution de promesse', async () => { const data = await Promise.resolve('data'); expect(data).toBe('data'); }); });

Mocking avec Jest

Jest excelle dans la création de mocks pour isoler les composants testés :

Mocking d'API avec Jest
// src/users.js const axios = require('axios'); async function getUsers() { try { const response = await axios.get('https://api.example.com/users'); return response.data; } catch (error) { console.error('Erreur lors de la récupération des utilisateurs:', error); return []; } } async function getUserById(id) { try { const response = await axios.get(`https://api.example.com/users/${id}`); return response.data; } catch (error) { console.error(`Erreur lors de la récupération de l'utilisateur ${id}:`, error); return null; } } module.exports = { getUsers, getUserById }; // tests/users.test.js const axios = require('axios'); const { getUsers, getUserById } = require('../src/users'); // Mock complet du module axios jest.mock('axios'); describe('Service utilisateurs', () => { // Réinitialiser les mocks après chaque test afterEach(() => { jest.resetAllMocks(); }); describe('getUsers', () => { it('renvoie les données des utilisateurs lorsque la requête réussit', async () => { // Configurer le mock pour simuler une réponse réussie const users = [ { id: 1, name: 'John Doe' }, { id: 2, name: 'Jane Smith' } ]; axios.get.mockResolvedValue({ data: users }); // Appeler la fonction et vérifier le résultat const result = await getUsers(); // Vérifications expect(axios.get).toHaveBeenCalledWith('https://api.example.com/users'); expect(result).toEqual(users); }); it('renvoie un tableau vide lorsque la requête échoue', async () => { // Configurer le mock pour simuler une erreur axios.get.mockRejectedValue(new Error('Network Error')); // Espionner console.error console.error = jest.fn(); // Appeler la fonction et vérifier le résultat const result = await getUsers(); // Vérifications expect(axios.get).toHaveBeenCalledWith('https://api.example.com/users'); expect(console.error).toHaveBeenCalled(); expect(result).toEqual([]); }); }); describe('getUserById', () => { it('renvoie les données d'un utilisateur lorsque la requête réussit', async () => { // Configurer le mock const user = { id: 1, name: 'John Doe' }; axios.get.mockResolvedValue({ data: user }); // Appeler la fonction const result = await getUserById(1); // Vérifications expect(axios.get).toHaveBeenCalledWith('https://api.example.com/users/1'); expect(result).toEqual(user); }); }); });

Tests de snapshots

Les snapshots permettent de tester contre des captures précédentes pour détecter les changements :

Tests de snapshots et tests de composants React
// src/components/Button.js import React from 'react'; const Button = ({ text, onClick, disabled }) => ( <button className={`btn ${disabled ? 'btn-disabled' : 'btn-primary'}`} onClick={onClick} disabled={disabled} > {text} </button> ); export default Button; // tests/components/Button.test.js import React from 'react'; import renderer from 'react-test-renderer'; import Button from '../../src/components/Button'; describe('Composant Button', () => { it('correspond au snapshot - état normal', () => { const handleClick = jest.fn(); const component = renderer.create( <Button text="Cliquez-moi" onClick={handleClick} disabled={false} /> ); // Générer un instantané et le comparer avec la version précédente const tree = component.toJSON(); expect(tree).toMatchSnapshot(); }); it('correspond au snapshot - état désactivé', () => { const handleClick = jest.fn(); const component = renderer.create( <Button text="Désactivé" onClick={handleClick} disabled={true} /> ); const tree = component.toJSON(); expect(tree).toMatchSnapshot(); }); }); // Exemple d'utilisation avec @testing-library/react import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import Button from '../../src/components/Button'; describe('Composant Button avec Testing Library', () => { it('affiche le texte correctement', () => { render(<Button text="Test Button" onClick={() => {}} disabled={false} />); expect(screen.getByText('Test Button')).toBeInTheDocument(); }); it('appelle la fonction onClick lorsqu'on clique sur le bouton', () => { const handleClick = jest.fn(); render(<Button text="Cliquez-moi" onClick={handleClick} disabled={false} />); fireEvent.click(screen.getByText('Cliquez-moi')); expect(handleClick).toHaveBeenCalledTimes(1); }); it('est désactivé lorsque la prop disabled est true', () => { render(<Button text="Désactivé" onClick={() => {}} disabled={true} />); const button = screen.getByText('Désactivé'); expect(button).toBeDisabled(); expect(button).toHaveClass('btn-disabled'); }); });

Tests d'intégration

Les tests d'intégration vérifient l'interaction entre plusieurs unités :

Tests d'intégration d'un formulaire React
// src/utils/auth.js export function validateEmail(email) { const re = /^[\w-]+(\.[\w-]+)*@([\w-]+\.)+[a-zA-Z]{2,7}$/; return re.test(String(email).toLowerCase()); } export function validatePassword(password) { // Au moins 8 caractères, 1 majuscule, 1 minuscule, 1 chiffre const re = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,}$/; return re.test(password); } // src/components/LoginForm.js import React, { useState } from 'react'; import { validateEmail, validatePassword } from '../utils/auth'; const LoginForm = ({ onSubmit }) => { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [errors, setErrors] = useState({}); const handleSubmit = (e) => { e.preventDefault(); // Validation const newErrors = {}; if (!validateEmail(email)) { newErrors.email = 'Email invalide'; } if (!validatePassword(password)) { newErrors.password = 'Le mot de passe doit contenir au moins 8 caractères, une majuscule, une minuscule et un chiffre'; } if (Object.keys(newErrors).length > 0) { setErrors(newErrors); return; } // Si tout est valide, soumettre le formulaire onSubmit({ email, password }); }; return ( <form onSubmit={handleSubmit} data-testid="login-form"> <div className="form-group"> <label htmlFor="email">Email</label> <input type="email" id="email" value={email} onChange={(e) => setEmail(e.target.value)} className={`form-control ${errors.email ? 'is-invalid' : ''}`} data-testid="email-input" /> {errors.email && <div className="error-message" data-testid="email-error">{errors.email}</div>} </div> <div className="form-group"> <label htmlFor="password">Mot de passe</label> <input type="password" id="password" value={password} onChange={(e) => setPassword(e.target.value)} className={`form-control ${errors.password ? 'is-invalid' : ''}`} data-testid="password-input" /> {errors.password && <div className="error-message" data-testid="password-error">{errors.password}</div>} </div> <button type="submit" className="btn btn-primary" data-testid="submit-button"> Se connecter </button> </form> ); }; export default LoginForm; // tests/components/LoginForm.test.js import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import LoginForm from '../../src/components/LoginForm'; import * as authUtils from '../../src/utils/auth'; // Espionner les fonctions de validation jest.spyOn(authUtils, 'validateEmail'); jest.spyOn(authUtils, 'validatePassword'); describe('Composant LoginForm', () => { beforeEach(() => { // Réinitialiser les mocks avant chaque test jest.clearAllMocks(); }); it('affiche le formulaire correctement', () => { render(<LoginForm onSubmit={() => {}} />); expect(screen.getByLabelText(/email/i)).toBeInTheDocument(); expect(screen.getByLabelText(/mot de passe/i)).toBeInTheDocument(); expect(screen.getByTestId('submit-button')).toBeInTheDocument(); }); it('appelle la fonction onSubmit avec les données correctes lorsque le formulaire est valide', () => { // Forcer les validations à retourner true authUtils.validateEmail.mockReturnValue(true); authUtils.validatePassword.mockReturnValue(true); const mockSubmit = jest.fn(); render(<LoginForm onSubmit={mockSubmit} />); // Remplir le formulaire fireEvent.change(screen.getByTestId('email-input'), { target: { value: 'test@example.com' } }); fireEvent.change(screen.getByTestId('password-input'), { target: { value: 'Password123' } }); // Soumettre le formulaire fireEvent.submit(screen.getByTestId('login-form')); // Vérifications expect(authUtils.validateEmail).toHaveBeenCalledWith('test@example.com'); expect(authUtils.validatePassword).toHaveBeenCalledWith('Password123'); expect(mockSubmit).toHaveBeenCalledWith({ email: 'test@example.com', password: 'Password123' }); }); it('affiche une erreur lorsque l'email est invalide', () => { // Simuler un email invalide authUtils.validateEmail.mockReturnValue(false); authUtils.validatePassword.mockReturnValue(true); render(<LoginForm onSubmit={() => {}} />); // Remplir le formulaire avec un email invalide fireEvent.change(screen.getByTestId('email-input'), { target: { value: 'invalid-email' } }); fireEvent.change(screen.getByTestId('password-input'), { target: { value: 'Password123' } }); // Soumettre le formulaire fireEvent.submit(screen.getByTestId('login-form')); // Vérifier que l'erreur est affichée expect(screen.getByTestId('email-error')).toBeInTheDocument(); expect(screen.getByTestId('email-error')).toHaveTextContent('Email invalide'); }); });

Configuration et démarrage

Pour commencer avec Jest, il suffit d'installer le package et de configurer un script de test dans votre package.json :

npm install --save-dev jest # ou yarn add --dev jest
Configuration minimale dans package.json
{ "scripts": { "test": "jest", "test:watch": "jest --watch", "test:coverage": "jest --coverage" } }

Configuration complète avec jest.config.js :

jest.config.js
module.exports = { // Répertoires où chercher les tests testMatch: ['**/__tests__/**/*.js?(x)', '**/?(*.)+(spec|test).js?(x)'], // Ignorer certains répertoires testPathIgnorePatterns: ['/node_modules/', '/build/', '/dist/'], // Configuration de l'environnement de test testEnvironment: 'jsdom', // 'node' ou 'jsdom' // Définir des transformations pour les fichiers (transpilation) transform: { '^.+\.jsx?$': 'babel-jest', }, // Configuration de la couverture de code collectCoverageFrom: [ 'src/**/*.{js,jsx}', '!src/**/*.test.{js,jsx}', '!**/node_modules/**', ], // Seuils de couverture coverageThreshold: { global: { statements: 80, branches: 80, functions: 80, lines: 80, }, }, // Configurer les mocks globaux setupFilesAfterEnv: ['<rootDir>/jest.setup.js'], // Aliases de modules pour les imports moduleNameMapper: { '^@/(.*)$': '<rootDir>/src/$1', '\.(css|less|scss)$': 'identity-obj-proxy', }, };

Bonnes pratiques avec Jest

  • Structure des tests - Organisez vos tests avec describe et it/test pour une meilleure lisibilité
  • Nommage des tests - Utilisez des noms clairs qui décrivent le comportement attendu
  • Isolation - Assurez-vous que chaque test est indépendant et peut s'exécuter seul
  • Arrange-Act-Assert - Structurez vos tests selon ce pattern : préparer, agir, vérifier
  • Mocks à utiliser avec parcimonie - Ne moquez que ce qui est nécessaire pour isoler l'unité testée
  • Tests de snapshot - Utilisez-les pour les structures qui changent rarement, examinez attentivement les changements
  • Intégration continue - Exécutez Jest dans votre pipeline CI pour détecter les problèmes au plus tôt
  • Coverage minimal - Visez au moins 80% de couverture de code pour les parties critiques

Extensions et intégrations

Icône JestJest s'intègre bien avec d'autres outils pour enrichir votre expérience de test :

  • @testing-library/react - Pour tester les composants Icône ReactReact d'une manière orientée utilisateur
  • @testing-library/vue - Équivalent pour Icône Vue.jsVue.js
  • jest-dom - Matchers additionnels pour tester le DOM
  • jest-extended - Matchers supplémentaires pour des assertions plus expressives
  • jest-fetch-mock - Pour mocker l'API fetch
  • jest-axe - Pour tester l'accessibilité
  • Icône ESLintESLint avec plugin Jest - Pour respecter les bonnes pratiques de test

Jest est un outil extrêmement puissant et flexible qui peut s'adapter à différents types de projets JavaScript, des petites bibliothèques aux grandes applications React, Vue ou Angular. Sa philosophie "batteries included" en fait un choix idéal pour démarrer rapidement avec les tests, tout en offrant la flexibilité nécessaire pour des scénarios de test avancés.

Applications concrètes

Cas d'usage

Tests unitaires pour bibliothèques JS

Validation des fonctions et classes individuelles d'une bibliothèque JavaScript, assurant que chaque unité fonctionne correctement de manière isolée et que les futurs changements ne cassent pas les fonctionnalités existantes.

Tests de composants React

Vérification du rendu, des interactions et du comportement des composants React, avec la possibilité de tester en isolation via des mocks ou de manière plus intégrée, assurant ainsi une expérience utilisateur cohérente.

Validation de la logique métier

Tests des fonctions qui encapsulent la logique métier critique d'une application, comme les calculs financiers, les transformations de données ou les algorithmes complexes, garantissant leur exactitude et leur robustesse.

Snapshots pour l'UI

Utilisation des tests de snapshots pour capturer et vérifier l'état de l'interface utilisateur, permettant de détecter rapidement les changements visuels non intentionnels et d'assurer la cohérence de l'apparence.

Mocking d'API

Simulation des appels API externes pour tester le comportement de l'application dans différents scénarios (réussite, échec, lenteur), sans dépendre de services externes et en contrôlant parfaitement les conditions de test.

Intégration continue

Exécution automatique des tests Jest dans les pipelines CI/CD, bloquant les déploiements si les tests échouent et fournissant des rapports de couverture, contribuant ainsi à maintenir une qualité de code élevée en continu.

Jest dans un workflow TDD

Jest s'intègre parfaitement dans un workflow de Test-Driven Development (TDD), où les tests sont écrits avant le code d'implémentation :

  1. Écrire un test qui échoue - Décrire le comportement attendu de la fonction ou du composant qui n'existe pas encore
  2. Exécuter le test et vérifier qu'il échoue - Le mode watch de Jest permet de voir les échecs en temps réel
  3. Écrire le code minimal pour faire passer le test - Implémenter juste ce qu'il faut pour que le test passe
  4. Exécuter le test et vérifier qu'il passe - Confirmer que l'implémentation satisfait les attentes
  5. Refactoriser le code - Améliorer l'implémentation tout en gardant les tests au vert
  6. Répéter pour les fonctionnalités suivantes - Continuer le cycle pour chaque nouvelle fonctionnalité

Ce workflow garantit que chaque ligne de code écrite est intentionnelle et répond à un besoin spécifique, tout en maintenant une couverture de tests optimale. Le mode watch de Jest, qui détecte automatiquement les fichiers modifiés et exécute uniquement les tests pertinents, est particulièrement bien adapté à cette approche.