Introduction aux tests
Pourquoi et comment tester une application React
Écrire du code qui marche, c'est bien. Écrire du code qui continue de marcher quand on le modifie, c'est mieux. C'est tout l'intérêt des tests automatisés.
Dans une application React, on distingue principalement trois types de tests :
- Tests unitaires : on teste une fonction ou un module isolé. Pas de DOM, pas de navigateur. C'est rapide, simple, et ça couvre la logique métier pure (un reducer, un schéma zod, une fonction utilitaire).
- Tests de composants : on monte un composant React dans un environnement simulé et on vérifie qu'il s'affiche et se comporte correctement. C'est le cœur du testing en React.
- Tests end-to-end (E2E) : on lance l'application dans un vrai navigateur et on simule un parcours utilisateur complet (Cypress, Playwright). C'est le plus réaliste, mais aussi le plus lent et le plus fragile.
On va se concentrer sur les deux premiers : les tests unitaires et les tests de composants, qui représentent 90% des tests que vous écrirez au quotidien.
L'outil : Vitest
Vitest est le standard actuel pour tester des applications React modernes. Il remplace Jest, qui était le standard historique, avec une compatibilité quasi totale mais des performances bien meilleures (il utilise le même pipeline que Vite).
$ npm install -D vitest @testing-library/react @testing-library/jest-dom jsdomLa configuration se fait dans le vite.config.ts :
/// <reference types="vitest" />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom', /* Simule un navigateur pour les tests de composants */
globals: true, /* describe, it, expect disponibles sans import */
setupFiles: './src/test/setup.ts',
},
});Et le fichier de setup pour intégrer les matchers de Testing Library :
/* src/test/setup.ts */
import '@testing-library/jest-dom';Tests unitaires
Le test unitaire est le plus simple à écrire. On teste une fonction pure : on lui donne une entrée, on vérifie la sortie.
Tester une fonction utilitaire
/* utils/formatPrice.ts */
export function formatPrice(price: number): string {
return new Intl.NumberFormat('fr-FR', {
style: 'currency',
currency: 'EUR',
}).format(price);
}/* utils/formatPrice.test.ts */
import { describe, it, expect } from 'vitest';
import { formatPrice } from './formatPrice';
describe('formatPrice', () => {
it('devrait formater un prix en euros', () => {
expect(formatPrice(29.99)).toBe('29,99 €');
});
it('devrait gérer les nombres entiers', () => {
expect(formatPrice(100)).toBe('100,00 €');
});
it('devrait gérer zéro', () => {
expect(formatPrice(0)).toBe('0,00 €');
});
});Tester un schéma zod
On a vu dans le chapitre sur zod que les schémas sont des fonctions pures. Ils sont donc parfaits pour les tests unitaires : on leur donne des données, ils disent si c'est valide ou non. Pas de mock, pas de DOM.
/* entities/product/model/productSchema.test.ts */
import { describe, it, expect } from 'vitest';
import { ProductSchema } from './productSchema';
describe('ProductSchema', () => {
it('devrait accepter un produit valide', () => {
const validProduct = {
id: '1',
name: 'T-shirt',
price: 29.99,
imageUrl: 'https://example.com/tshirt.jpg',
};
const result = ProductSchema.safeParse(validProduct);
expect(result.success).toBe(true);
});
it('devrait rejeter un produit avec un prix en string', () => {
const invalidProduct = {
id: '1',
name: 'T-shirt',
price: '29.99',
imageUrl: 'https://example.com/tshirt.jpg',
};
const result = ProductSchema.safeParse(invalidProduct);
expect(result.success).toBe(false);
});
it('devrait rejeter un produit sans nom', () => {
const result = ProductSchema.safeParse({
id: '1',
price: 29.99,
imageUrl: 'https://example.com/tshirt.jpg',
});
expect(result.success).toBe(false);
});
});Tester les messages d'erreur personnalisés
Si vous avez défini des messages de validation dans vos schémas, vous pouvez vérifier qu'ils sont bien retournés :
import { RegisterSchema } from './registerSchema';
it('devrait retourner le bon message si les mots de passe ne correspondent pas', () => {
const result = RegisterSchema.safeParse({
email: 'test@test.com',
password: 'Password1',
confirmPassword: 'Password2',
});
expect(result.success).toBe(false);
if (!result.success) {
const confirmError = result.error.issues.find(
(issue) => issue.path.includes('confirmPassword')
);
expect(confirmError?.message).toBe('Les mots de passe ne correspondent pas');
}
});Tester un store Zustand
Les stores Zustand sont aussi testables unitairement. On peut directement appeler les actions et vérifier l'état résultant sans monter aucun composant :
/* store/useCartStore.test.ts */
import { describe, it, expect, beforeEach } from 'vitest';
import { useCartStore } from './useCartStore';
describe('useCartStore', () => {
beforeEach(() => {
/* On remet le store à zéro entre chaque test */
useCartStore.setState({ items: [] });
});
it('devrait ajouter un article au panier', () => {
const { addItem } = useCartStore.getState();
addItem({ id: '1', name: 'T-shirt', price: 29.99 });
const { items } = useCartStore.getState();
expect(items).toHaveLength(1);
expect(items[0].name).toBe('T-shirt');
});
it('devrait supprimer un article du panier', () => {
useCartStore.setState({
items: [{ id: '1', name: 'T-shirt', price: 29.99 }],
});
const { removeItem } = useCartStore.getState();
removeItem('1');
const { items } = useCartStore.getState();
expect(items).toHaveLength(0);
});
});Il vaut mieux utiliser getState() pour accéder à l'état et aux actions directement, sans passer par un hook React.
On utilise setState() dans le beforeEach pour repartir d'un état propre à chaque test.