Comprends la philosophie et les outils pour tester les composants React, incluant React Testing Library, les requêtes centrées sur l'utilisateur et les bonnes pratiques pour écrire des tests maintenables.
Les tests sont ton filet de sécurité. Sans tests, chaque modification que tu apportes à ton code est un pari — tu pourrais corriger une chose et en casser trois autres sans le savoir. Les tests te donnent la confiance que tes composants se comportent correctement et continuent de fonctionner lorsque tu refactorises, ajoutes des fonctionnalités ou mets à jour des dépendances.
Il existe trois niveaux principaux de tests :
1. Tests unitaires — Testent des fonctions ou composants individuels de manière isolée. Rapides à exécuter, faciles à écrire, mais peuvent manquer des problèmes d'intégration.
// Tester une fonction utilitaire
expect(formatPrice(1999)).toBe('$19.99');2. Tests d'intégration — Testent comment plusieurs composants fonctionnent ensemble. C'est le sweet spot pour React — tester un composant de formulaire qui inclut des champs de saisie, de la validation et de la soumission.
// Tester un formulaire de connexion
render(<LoginForm />);
await user.type(screen.getByLabelText('Email'), 'test@example.com');
await user.click(screen.getByRole('button', { name: 'Log in' }));
expect(screen.getByText('Welcome!')).toBeInTheDocument();3. Tests end-to-end (E2E) — Testent l'application entière du point de vue de l'utilisateur avec des outils comme Playwright ou Cypress. Lents et coûteux, mais détectent les problèmes réels.
Le testing trophy (popularisé par Kent C. Dodds) suggère d'écrire principalement des tests d'intégration, quelques tests unitaires, moins de tests E2E et un minimum d'analyse statique. Cela donne le meilleur ratio confiance/effort.
/\ E2E (peu)
/ \ Intégration (beaucoup)
/ \ Unitaires (quelques-uns)
/------\ Statique (TypeScript, ESLint)React Testing Library (RTL) est l'outil standard pour tester les composants React. Il est construit sur un principe simple :
« Plus tes tests ressemblent à la façon dont ton logiciel est utilisé, plus ils peuvent te donner confiance. »
Cela signifie tester le comportement, pas l'implémentation. Tu ne devrais pas tester si une variable d'état a été définie à true ou si une fonction spécifique a été appelée. Au lieu de cela, teste ce que l'utilisateur voit et fait :
Mauvais test (détail d'implémentation) :
// Tester l'état interne — fragile, casse lors du refactoring
const { result } = renderHook(() => useCounter());
act(() => result.current.increment());
expect(result.current.count).toBe(1);Bon test (comportement) :
// Tester ce que l'utilisateur voit — résistant au refactoring
render(<Counter />);
await user.click(screen.getByRole('button', { name: 'Increment' }));
expect(screen.getByText('Count: 1')).toBeInTheDocument();Les deux tests vérifient que le compteur fonctionne, mais le test de comportement survivra au refactoring (renommer le hook, changer la gestion d'état) tandis que le test d'implémentation cassera.
RTL fournit une fonction render qui monte ton composant dans un DOM de test, et un objet screen pour interroger ce qui a été rendu. Il n'expose intentionnellement pas les détails internes du composant comme l'état ou les props.
React Testing Library fournit plusieurs méthodes de requête, priorisées selon leur accessibilité :
Priorité 1 : Accessible à tous
getByRole('button') — Interroge par rôle ARIA. Meilleur choix car il correspond à la façon dont les lecteurs d'écran et les utilisateurs perçoivent les éléments.getByLabelText('Email') — Interroge les éléments de formulaire par leur label associé. Idéal pour les champs de formulaire.getByPlaceholderText('Search...') — Interroge par placeholder. À utiliser uniquement s'il n'y a pas de label.getByText('Submit') — Interroge par contenu texte visible.getByDisplayValue('john@example.com') — Interroge par la valeur actuelle d'un input.Priorité 2 : Requêtes sémantiques
getByAltText('User avatar') — Interroge les images par texte alt.getByTitle('Close') — Interroge par l'attribut title.Priorité 3 : Test IDs (dernier recours)
getByTestId('submit-button') — Interroge par attribut data-testid. À utiliser quand aucune autre requête ne fonctionne.// Composant
function UserCard({ name, role }) {
return (
<div data-testid="user-card">
<h3>{name}</h3>
<p>{role}</p>
</div>
);
}
// Test
render(<UserCard name="Jane" role="Developer" />);
expect(screen.getByText('Jane')).toBeInTheDocument();
expect(screen.getByText('Developer')).toBeInTheDocument();
expect(screen.getByTestId('user-card')).toBeInTheDocument();Chaque requête getBy lance une erreur si l'élément n'est pas trouvé. Utilise queryBy quand tu t'attends à ce que l'élément n'existe pas (retourne null au lieu de lancer une erreur). Utilise findBy pour les éléments qui apparaissent de manière asynchrone (retourne une Promise).
// Affirmer que quelque chose n'est PAS présent
expect(screen.queryByText('Error')).not.toBeInTheDocument();
// Attendre du contenu asynchrone
const message = await screen.findByText('Data loaded');React Testing Library encourage le test des interactions utilisateur avec la bibliothèque @testing-library/user-event, qui simule les événements navigateur réels plus précisément que fireEvent :
import userEvent from '@testing-library/user-event';
test('form submission', async () => {
const user = userEvent.setup();
render(<LoginForm />);
// Taper dans les inputs
await user.type(screen.getByLabelText('Email'), 'test@example.com');
await user.type(screen.getByLabelText('Password'), 'secret123');
// Cliquer sur soumettre
await user.click(screen.getByRole('button', { name: 'Log in' }));
// Affirmer le résultat
expect(screen.getByText('Welcome, test@example.com!')).toBeInTheDocument();
});user.type() simule chaque frappe individuellement, déclenchant les événements keydown, keypress, input et keyup — exactement comme un utilisateur réel qui tape. fireEvent.change() ne déclenche que l'événement change, ce qui peut manquer des bugs.
Assertions courantes de @testing-library/jest-dom :
// Présence d'élément
expect(element).toBeInTheDocument();
expect(element).not.toBeInTheDocument();
// Visibilité
expect(element).toBeVisible();
expect(element).toHaveStyle('display: none');
// Contenu
expect(element).toHaveTextContent('Hello');
expect(input).toHaveValue('test@example.com');
// Attributs
expect(button).toBeDisabled();
expect(input).toBeRequired();
expect(element).toHaveAttribute('href', '/about');
expect(element).toHaveClass('active');L'attribut data-testid est ta porte de sortie pour interroger des éléments qui n'ont pas de rôle accessible, de label ou de texte. Il n'est pas visible pour les utilisateurs et n'affecte pas le comportement :
<div data-testid="user-card">
<h3 data-testid="user-name">Jane</h3>
</div>Utilise data-testid avec parcimonie — privilégie d'abord les requêtes accessibles.
Le mocking remplace les dépendances réelles par des substituts contrôlés pendant les tests. C'est essentiel pour isoler le composant testé :
Mocker les appels API :
// Mocker la fonction fetch
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ name: 'Jane', role: 'Developer' }),
})
);
test('displays user data', async () => {
render(<UserProfile userId="1" />);
expect(await screen.findByText('Jane')).toBeInTheDocument();
});Mocker les composants enfants :
// Remplacer un composant enfant lourd par un mock simple
jest.mock('./HeavyChart', () => {
return function MockChart() {
return <div data-testid="chart">Chart placeholder</div>;
};
});Mocker les providers de contexte :
function renderWithTheme(ui, theme = 'light') {
return render(
<ThemeContext.Provider value={{ theme }}>
{ui}
</ThemeContext.Provider>
);
}
test('renders dark theme', () => {
renderWithTheme(<Header />, 'dark');
expect(screen.getByRole('banner')).toHaveClass('dark');
});Les tests de snapshot capturent le rendu d'un composant et le comparent à un fichier "snapshot" stocké. Si le rendu change, le test échoue :
test('matches snapshot', () => {
const { container } = render(<UserCard name="Jane" role="Dev" />);
expect(container).toMatchSnapshot();
});Les snapshots sont controversés. Ils sont faciles à créer mais peuvent mener à une acceptation aveugle de "update snapshot" quand ils cassent. Utilise-les pour des composants présentationnels stables et privilégie les assertions explicites pour les composants interactifs.
Bonne pratique : Écris des tests qui échoueraient si le comportement changeait mais qui réussiraient si seule l'implémentation changeait. C'est l'idée clé derrière React Testing Library.
<div id="root"></div>
<script src="https://unpkg.com/react@19/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@19/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
// Un composant bien structuré et testable
// Remarque : HTML sémantique, data-testid pour les requêtes complexes, props pour les données
function UserCard({ name, role }) {
return (
<div data-testid="user-card">
<h3 data-testid="user-name">{name}</h3>
<p data-testid="user-role">{role}</p>
</div>
);
}
function UserList({ users }) {
if (users.length === 0) {
return <p data-testid="empty-message">No users found.</p>;
}
return (
<div data-testid="user-list">
{users.map((user, i) => (
<UserCard key={i} name={user.name} role={user.role} />
))}
</div>
);
}
function App() {
const users = [
{ name: "Jane", role: "Developer" },
{ name: "Alex", role: "Designer" },
];
return (
<div>
<h1>Team Members</h1>
<UserList users={users} />
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>Quelle requête devrais-tu privilégier en premier lors du test d'un bouton avec React Testing Library ?