Tester les composants
Tests de composants React avec Testing Library
Les tests unitaires couvrent la logique pure, mais dans une application React, la majeure partie du code vit dans des composants. Il faut pouvoir vérifier qu'un composant s'affiche correctement et réagit aux interactions de l'utilisateur.
C'est le rôle de React Testing Library. Sa philosophie est simple : tester les composants comme un utilisateur les utiliserait, pas comme un développeur les a codés. On ne teste pas l'implémentation interne (l'état, les refs, les hooks), on teste le rendu visible et les interactions.
Monter un composant
La fonction render de Testing Library monte votre composant dans un DOM simulé (jsdom). Ensuite, on utilise screen pour chercher des éléments comme le ferait un utilisateur : par le texte visible, les labels, les rôles ARIA.
import { render, screen } from '@testing-library/react';
import { ProductCard } from './ProductCard';
describe('ProductCard', () => {
it('devrait afficher le nom et le prix du produit', () => {
render(
<ProductCard
product={{ id: '1', name: 'T-shirt', price: 29.99, imageUrl: '/img.jpg' }}
/>
);
expect(screen.getByText('T-shirt')).toBeInTheDocument();
expect(screen.getByText('29,99 €')).toBeInTheDocument();
});
});Simuler les interactions
Pour simuler un clic, une saisie de texte ou toute autre interaction, on utilise userEvent. C'est plus réaliste que fireEvent car il simule le comportement complet de l'utilisateur (focus, frappe caractère par caractère, etc.).
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
describe('AddToCartButton', () => {
it('devrait appeler onAdd au clic', async () => {
const mockOnAdd = vi.fn();
render(<AddToCartButton onAdd={mockOnAdd} />);
const button = screen.getByRole('button', { name: /ajouter/i });
await userEvent.click(button);
expect(mockOnAdd).toHaveBeenCalledOnce();
});
});Quelques points importants :
- On utilise
getByRoleavec un nom accessible plutôt que de chercher par classe CSS ou pardata-testid. C'est la manière recommandée. vi.fn()crée une fonction mock de Vitest qu'on peut ensuite inspecter (nombre d'appels, arguments, etc.).userEventest asynchrone, d'où leasync/await.
Tester le rendu conditionnel
Un cas très courant : vérifier qu'un composant affiche ou masque des éléments en fonction de son état.
describe('StockBadge', () => {
it('devrait afficher "En stock" quand le stock est positif', () => {
render(<StockBadge stock={5} />);
expect(screen.getByText('En stock')).toBeInTheDocument();
});
it('devrait afficher "Rupture de stock" quand le stock est à zéro', () => {
render(<StockBadge stock={0} />);
expect(screen.getByText('Rupture de stock')).toBeInTheDocument();
});
it('ne devrait pas afficher le bouton d\'achat en rupture', () => {
render(<StockBadge stock={0} />);
expect(screen.queryByRole('button', { name: /acheter/i })).not.toBeInTheDocument();
});
});La différence entre getByText et queryByText est importante :
getBy...lève une erreur si l'élément n'existe pas (utile quand on sait qu'il devrait être là).queryBy...retournenullsi l'élément n'existe pas (utile pour vérifier l'absence d'un élément).
Tester un composant qui utilise un store Zustand
Si votre composant consomme un store Zustand, vous n'avez rien de spécial à configurer. Pas de Provider à envelopper. Il suffit d'initialiser le store dans l'état souhaité avant le test :
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { useCartStore } from '@/store/useCartStore';
import { CartBadge } from './CartBadge';
describe('CartBadge', () => {
beforeEach(() => {
useCartStore.setState({ items: [] });
});
it('devrait afficher le nombre d\'articles', () => {
useCartStore.setState({
items: [
{ id: '1', name: 'T-shirt', price: 29.99 },
{ id: '2', name: 'Casquette', price: 15 },
],
});
render(<CartBadge />);
expect(screen.getByText('2')).toBeInTheDocument();
});
it('ne devrait rien afficher si le panier est vide', () => {
render(<CartBadge />);
expect(screen.queryByText('0')).not.toBeInTheDocument();
});
});C'est un des avantages de Zustand par rapport au Context API : pas de Provider à wrapper autour du composant dans les tests.
Tester un composant avec React Query
Les composants qui utilisent React Query ont besoin d'un QueryClientProvider. On crée un utilitaire de rendu dédié pour éviter de le répéter partout :
/* test/renderWithProviders.tsx */
import { render } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
export function renderWithProviders(ui: React.ReactElement) {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false }, /* Pas de retry dans les tests */
},
});
return render(
<QueryClientProvider client={queryClient}>
{ui}
</QueryClientProvider>
);
}import { renderWithProviders } from '@/test/renderWithProviders';
import { ProductList } from './ProductList';
describe('ProductList', () => {
it('devrait afficher un loader pendant le chargement', () => {
renderWithProviders(<ProductList />);
expect(screen.getByText(/chargement/i)).toBeInTheDocument();
});
});Les bonnes pratiques
Quelques règles à garder en tête pour écrire des tests maintenables :
- Testez le comportement, pas l'implémentation. Ne vérifiez jamais qu'un
useStatea une certaine valeur. Vérifiez que le texte affiché à l'écran est correct. - Utilisez les rôles et labels accessibles (
getByRole,getByLabelText) plutôt que lesdata-testidou les sélecteurs CSS. C'est plus proche de l'expérience utilisateur et ça vous pousse à écrire du HTML accessible. - Un test = un comportement. Chaque
itdevrait tester une seule chose précise. - Nommez vos tests comme des phrases.
'devrait afficher le prix en euros'est plus lisible que'test price formatting'.
Avec FSD
Dans une architecture FSD, les fichiers de test se placent à côté du fichier qu'ils testent, avec le suffixe .test.ts ou .test.tsx :
entities/product/
├── model/
│ ├── productSchema.ts
│ └── productSchema.test.ts /* Test unitaire du schéma */
├── ui/
│ ├── ProductCard.tsx
│ └── ProductCard.test.tsx /* Test de composant */
└── index.tsL'idée c'est de rendre les tests faciles à trouver et à maintenir. Quand on modifie un fichier, son test est juste à côté.