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.
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.
Fonctionnement technique
Jest est un framework de test
JavaScript 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
React 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/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 :
// 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 :
// 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 :
// 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
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
}
Configuration complète avec 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
Jest s'intègre bien avec d'autres outils pour enrichir votre expérience de test :
- @testing-library/react - Pour tester les composants
React d'une manière orientée utilisateur
- @testing-library/vue - Équivalent pour
Vue.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é
ESLint 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.
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 :
- Écrire un test qui échoue - Décrire le comportement attendu de la fonction ou du composant qui n'existe pas encore
- Exécuter le test et vérifier qu'il échoue - Le mode watch de Jest permet de voir les échecs en temps réel
- Écrire le code minimal pour faire passer le test - Implémenter juste ce qu'il faut pour que le test passe
- Exécuter le test et vérifier qu'il passe - Confirmer que l'implémentation satisfait les attentes
- Refactoriser le code - Améliorer l'implémentation tout en gardant les tests au vert
- 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.