Guide complet des tests en JavaScript
Les tests sont un aspect fondamental du développement logiciel professionnel. Ils garantissent que votre code fonctionne comme prévu, facilitent la maintenance et permettent de détecter les régressions. Cet article explore les différentes approches et outils pour tester efficacement vos applications JavaScript.
Pourquoi tester votre code ?
- Confiance : Assurance que votre code fonctionne correctement
- Documentation : Les tests servent de documentation vivante
- Refactoring sécurisé : Modification du code sans craindre de casser des fonctionnalités
- Qualité : Réduction des bugs et amélioration de la conception
- Collaboration : Facilitation du travail en équipe
- Déploiement continu : Automatisation des vérifications avant déploiement
Types de tests
Tests unitaires
Les tests unitaires vérifient le bon fonctionnement des plus petites unités de code (fonctions, méthodes, classes) de manière isolée.
// Fonction à tester
function somme(a, b) {
return a + b;
}
// Test unitaire avec Jest
test('additionne correctement deux nombres', () => {
expect(somme(2, 3)).toBe(5);
expect(somme(-1, 1)).toBe(0);
expect(somme(0, 0)).toBe(0);
});
Tests d’intégration
Les tests d’intégration vérifient que différentes parties du système fonctionnent correctement ensemble.
// Test d'intégration avec Jest
describe('Service utilisateur', () => {
test('crée un utilisateur et le récupère de la base de données', async () => {
// Créer un utilisateur
const userId = await userService.createUser({
name: 'Alice',
email: 'alice@example.com'
});
// Récupérer l'utilisateur
const user = await userService.getUserById(userId);
// Vérifier que l'utilisateur est correctement récupéré
expect(user).toEqual({
id: userId,
name: 'Alice',
email: 'alice@example.com'
});
});
});
Tests fonctionnels / End-to-End (E2E)
Les tests E2E vérifient le comportement de l’application du point de vue de l’utilisateur final.
// Test E2E avec Cypress
describe('Formulaire de connexion', () => {
it('permet à un utilisateur de se connecter', () => {
cy.visit('/login');
cy.get('input[name="email"]').type('utilisateur@exemple.fr');
cy.get('input[name="password"]').type('motdepasse123');
cy.get('button[type="submit"]').click();
// Vérifier que l'utilisateur est redirigé vers le tableau de bord
cy.url().should('include', '/dashboard');
cy.contains('Bienvenue, Utilisateur').should('be.visible');
});
});
Tests de composants
Pour les applications basées sur des composants (React, Vue, Angular), les tests de composants vérifient le rendu et le comportement des composants UI.
// Test de composant React avec React Testing Library
import { render, screen, fireEvent } from '@testing-library/react';
import Compteur from './Compteur';
test('incrémente le compteur lorsque le bouton est cliqué', () => {
render(<Compteur />);
// Vérifier l'état initial
expect(screen.getByText('Compteur: 0')).toBeInTheDocument();
// Cliquer sur le bouton
fireEvent.click(screen.getByText('Incrémenter'));
// Vérifier que le compteur a été incrémenté
expect(screen.getByText('Compteur: 1')).toBeInTheDocument();
});
Tests de performance
Les tests de performance vérifient que votre application répond aux exigences de vitesse et d’efficacité.
// Test de performance avec Lighthouse CI
module.exports = {
ci: {
collect: {
url: ['http://localhost:3000/'],
numberOfRuns: 3,
},
assert: {
assertions: {
'first-contentful-paint': ['warn', { maxNumericValue: 2000 }],
'interactive': ['error', { maxNumericValue: 3000 }],
'max-potential-fid': ['error', { maxNumericValue: 100 }],
},
},
upload: {
target: 'temporary-public-storage',
},
},
};
Tests de sécurité
Les tests de sécurité identifient les vulnérabilités potentielles dans votre application.
// Exemple avec OWASP ZAP (via CLI)
// zap-cli quick-scan --self-contained --start-options "-config api.disablekey=true" https://example.com
Frameworks et outils de test populaires
Jest
Jest est un framework de test JavaScript complet avec une configuration minimale.
// Installation
// npm install --save-dev jest
// Configuration minimale (package.json)
{
"scripts": {
"test": "jest"
}
}
// Exemple de test
const { somme } = require('./math');
describe('Fonctions mathématiques', () => {
test('somme additionne deux nombres', () => {
expect(somme(1, 2)).toBe(3);
});
test('somme gère les nombres négatifs', () => {
expect(somme(-1, -2)).toBe(-3);
});
test('somme avec zéro retourne l\'autre nombre', () => {
expect(somme(0, 5)).toBe(5);
});
});
Mocha + Chai
Mocha est un framework de test flexible, souvent utilisé avec Chai pour les assertions.
// Installation
// npm install --save-dev mocha chai
// Exemple de test
const { expect } = require('chai');
const { somme } = require('./math');
describe('Fonctions mathématiques', () => {
it('somme additionne deux nombres', () => {
expect(somme(1, 2)).to.equal(3);
});
it('somme gère les nombres négatifs', () => {
expect(somme(-1, -2)).to.equal(-3);
});
});
Cypress
Cypress est un outil de test E2E moderne qui s’exécute dans le navigateur.
// Installation
// npm install --save-dev cypress
// Exemple de test
describe('Page d\'accueil', () => {
beforeEach(() => {
cy.visit('/');
});
it('affiche le titre correct', () => {
cy.get('h1').should('contain', 'Bienvenue sur notre site');
});
it('navigue vers la page À propos', () => {
cy.get('nav').contains('À propos').click();
cy.url().should('include', '/about');
cy.get('h1').should('contain', 'À propos de nous');
});
});
Playwright
Playwright est un outil de test E2E qui prend en charge plusieurs navigateurs.
// Installation
// npm install --save-dev @playwright/test
// Exemple de test
const { test, expect } = require('@playwright/test');
test('page d\'accueil a le bon titre', async ({ page }) => {
await page.goto('https://example.com');
// Vérifier le titre
const title = await page.title();
expect(title).toBe('Example Domain');
// Vérifier le contenu
const heading = await page.textContent('h1');
expect(heading).toBe('Example Domain');
});
Testing Library
Testing Library est une famille de bibliothèques pour tester les interfaces utilisateur de manière centrée sur l’utilisateur.
// Installation pour React
// npm install --save-dev @testing-library/react @testing-library/jest-dom
// Exemple de test
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import Formulaire from './Formulaire';
test('soumet le formulaire avec les valeurs correctes', () => {
const handleSubmit = jest.fn();
render(<Formulaire onSubmit={handleSubmit} />);
// Remplir le formulaire
fireEvent.change(screen.getByLabelText(/nom/i), {
target: { value: 'Alice' },
});
fireEvent.change(screen.getByLabelText(/email/i), {
target: { value: 'alice@example.com' },
});
// Soumettre le formulaire
fireEvent.click(screen.getByRole('button', { name: /soumettre/i }));
// Vérifier que la fonction onSubmit a été appelée avec les bonnes valeurs
expect(handleSubmit).toHaveBeenCalledWith({
nom: 'Alice',
email: 'alice@example.com',
});
});
Storybook
Storybook permet de développer et tester des composants UI de manière isolée.
// Installation
// npx sb init
// Exemple de story
// Button.stories.js
import React from 'react';
import { Button } from './Button';
export default {
title: 'Components/Button',
component: Button,
argTypes: {
backgroundColor: { control: 'color' },
},
};
const Template = (args) => <Button {...args} />;
export const Primary = Template.bind({});
Primary.args = {
primary: true,
label: 'Button',
};
export const Secondary = Template.bind({});
Secondary.args = {
label: 'Button',
};
Approches de test
Test-Driven Development (TDD)
Le TDD suit un cycle en trois étapes :
- Red : Écrire un test qui échoue
- Green : Écrire le code minimal pour faire passer le test
- Refactor : Améliorer le code sans changer son comportement
// Exemple de TDD
// 1. Red: Écrire un test qui échoue
test('convertit les degrés Celsius en Fahrenheit', () => {
expect(celsiusToFahrenheit(0)).toBe(32);
expect(celsiusToFahrenheit(100)).toBe(212);
});
// 2. Green: Écrire le code minimal pour faire passer le test
function celsiusToFahrenheit(celsius) {
return celsius * 9/5 + 32;
}
// 3. Refactor: Améliorer le code (si nécessaire)
function celsiusToFahrenheit(celsius) {
const factor = 9/5;
const freezingPointDiff = 32;
return celsius * factor + freezingPointDiff;
}
Behavior-Driven Development (BDD)
Le BDD se concentre sur le comportement attendu du système du point de vue de l’utilisateur.
// Exemple avec Cucumber.js
// Feature: Connexion utilisateur
// En tant qu'utilisateur enregistré
// Je veux pouvoir me connecter
// Afin d'accéder à mon compte
// Scénario: Connexion réussie
// Étant donné que je suis sur la page de connexion
// Quand je saisis mon email "utilisateur@exemple.fr"
// Et je saisis mon mot de passe "motdepasse123"
// Et je clique sur le bouton "Se connecter"
// Alors je devrais être redirigé vers la page "Tableau de bord"
// Et je devrais voir "Bienvenue, Utilisateur"
// Implémentation des étapes
const { Given, When, Then } = require('@cucumber/cucumber');
Given('je suis sur la page de connexion', async function() {
await this.navigateTo('/login');
});
When('je saisis mon email {string}', async function(email) {
await this.fillField('email', email);
});
// ... autres étapes
Mocks, Stubs et Spies
Mocks
Les mocks remplacent des dépendances réelles par des versions simulées.
// Exemple avec Jest
test('envoie un email de bienvenue lors de l\'inscription', () => {
// Mock du service d'email
const emailService = {
sendWelcomeEmail: jest.fn()
};
// Système sous test
const userService = new UserService(emailService);
// Action
userService.registerUser('alice@example.com', 'password123');
// Assertion
expect(emailService.sendWelcomeEmail).toHaveBeenCalledWith('alice@example.com');
});
Stubs
Les stubs fournissent des réponses prédéfinies aux appels de fonction.
// Exemple avec Sinon.js
const sinon = require('sinon');
const database = require('./database');
test('récupère les données utilisateur', async () => {
// Créer un stub pour la méthode query
const queryStub = sinon.stub(database, 'query');
// Configurer le stub pour retourner des données spécifiques
queryStub.withArgs('SELECT * FROM users WHERE id = ?', [42])
.resolves([{ id: 42, name: 'Alice' }]);
// Système sous test
const userService = new UserService(database);
// Action
const user = await userService.getUserById(42);
// Assertion
expect(user).toEqual({ id: 42, name: 'Alice' });
// Restaurer le stub
queryStub.restore();
});
Spies
Les spies observent les appels de fonction sans modifier leur comportement.
// Exemple avec Jest
test('enregistre les erreurs dans la console', () => {
// Créer un spy sur console.error
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
// Action qui devrait générer une erreur
const calculator = new Calculator();
calculator.divide(10, 0);
// Assertion
expect(errorSpy).toHaveBeenCalledWith('Division par zéro');
// Restaurer le spy
errorSpy.mockRestore();
});
Tests asynchrones
Promises
// Test avec Jest
test('récupère les données utilisateur', () => {
return userService.getUserById(42)
.then(user => {
expect(user.name).toBe('Alice');
});
});
// Ou avec async/await
test('récupère les données utilisateur', async () => {
const user = await userService.getUserById(42);
expect(user.name).toBe('Alice');
});
Callbacks
// Test avec Jest
test('lit un fichier', done => {
fs.readFile('data.txt', 'utf8', (err, data) => {
if (err) {
done(err);
return;
}
try {
expect(data).toContain('Hello, world');
done();
} catch (error) {
done(error);
}
});
});
Couverture de code
La couverture de code mesure quelle partie de votre code est exécutée pendant les tests.
// Configuration Jest pour la couverture
// package.json
{
"jest": {
"collectCoverage": true,
"collectCoverageFrom": [
"src/**/*.{js,jsx}",
"!**/node_modules/**",
"!**/vendor/**"
],
"coverageThreshold": {
"global": {
"branches": 80,
"functions": 80,
"lines": 80,
"statements": 80
}
}
}
}
Intégration continue (CI)
L’intégration continue exécute automatiquement vos tests à chaque modification du code.
# Exemple avec GitHub Actions
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Node.js
uses: actions/setup-node@v2
with:
node-version: '16'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Upload coverage
uses: codecov/codecov-action@v2
Bonnes pratiques
- Testez d’abord les fonctionnalités critiques : Concentrez-vous sur le code le plus important
- Gardez les tests simples et lisibles : Un test doit être facile à comprendre
- Un test, une assertion : Chaque test devrait vérifier une seule chose
- Isolez vos tests : Les tests ne doivent pas dépendre les uns des autres
- Utilisez des données de test réalistes : Évitez les données trop simplistes
- Évitez les tests fragiles : Les tests ne doivent pas casser pour des raisons superficielles
- Exécutez les tests régulièrement : Idéalement à chaque modification du code
- Maintenez vos tests : Les tests sont du code et nécessitent de la maintenance
Pièges courants à éviter
- Tester l’implémentation plutôt que le comportement : Concentrez-vous sur ce que fait le code, pas comment il le fait
- Ignorer les cas limites : Testez les valeurs extrêmes et les cas d’erreur
- Tester uniquement le chemin heureux : N’oubliez pas de tester les cas d’échec
- Tests trop couplés au code : Les tests trop spécifiques cassent facilement lors des refactorings
- Négliger les tests de régression : Ajoutez des tests pour les bugs corrigés
- Tests lents : Des tests lents découragent leur exécution fréquente
Conclusion
Les tests sont un investissement qui paie des dividendes tout au long de la vie d’un projet. Ils améliorent la qualité du code, facilitent la maintenance et permettent des déploiements plus fréquents et plus fiables.
Il n’existe pas d’approche universelle pour les tests. La stratégie optimale dépend de la nature de votre projet, de votre équipe et de vos contraintes. L’important est de commencer quelque part et d’améliorer progressivement votre couverture de test.
En adoptant une culture de test dans votre équipe et en suivant les bonnes pratiques présentées dans cet article, vous pouvez considérablement améliorer la qualité et la maintenabilité de vos applications JavaScript.
À propos de InSkillCoach
Expert en formation et technologies
Coach spécialisé dans les technologies avancées et l'IA, porté par GNeurone Inc.
Certifications:
- AWS Certified Solutions Architect – Professional
- Certifications Google Cloud
- Microsoft Certified: DevOps Engineer Expert
- Certified Kubernetes Administrator (CKA)
- CompTIA Security+
Commentaires
Les commentaires sont alimentés par GitHub Discussions
Connectez-vous avec GitHub pour participer à la discussion