Apprenez à tester vos composants React avec React Testing Library, userEvent et MSW. Guide complet pour débutants : render, queries, formulaires, hooks.
Pourquoi tester ses composants React ?
Beaucoup de développeurs junior esquivent les tests — soit par manque de temps, soit parce qu'ils ne savent pas par où commencer. Résultat : des bugs en production, des régressions silencieuses, et une peur croissante de modifier le code existant. Pourtant, bien tester une application React n'est pas aussi complexe qu'on le croit, surtout avec React Testing Library (RTL).
Les 3 types de tests à connaître
En frontend React, on distingue trois niveaux de tests qui se complètent :
| Type | Ce qu'il teste | Outil principal | Vitesse | Coût maintenance |
|---|---|---|---|---|
| Unitaire | Fonction ou hook isolé | Jest / Vitest | ⚡ Très rapide | Faible |
| Intégration | Composant avec contexte réel | React Testing Library | 🚀 Rapide | Moyen |
| E2E | Parcours utilisateur complet | Playwright / Cypress | 🐢 Lent | Élevé |
React Testing Library se positionne comme l'outil d'intégration par excellence. Sa philosophie est unique : plutôt que de tester les détails d'implémentation (état interne, méthodes privées), RTL teste ce que voit et fait l'utilisateur. C'est cette approche qui la rend plus robuste et moins sujette aux faux positifs.
Ce que RTL NE fait pas
RTL ne remplace pas Jest (qui gère l'exécution et les assertions). La stack complète est : Jest (runner) + @testing-library/react (render + queries) + @testing-library/user-event (interactions) + @testing-library/jest-dom (matchers HTML).
Installation et configuration
Avec Create React App (déjà configuré)
Si vous utilisez Create React App, RTL est inclus par défaut. Vous pouvez directement
écrire vos tests dans des fichiers *.test.tsx.
Avec Vite + Vitest (stack recommandée)
Vitest est l'alternative moderne et ultra-rapide à Jest, pensée pour les projets Vite :
# Installation des dépendances
npm install --save-dev vitest jsdom @testing-library/react
npm install --save-dev @testing-library/user-event @testing-library/jest-dom
npm install --save-dev @vitejs/plugin-react
Configuration de Vitest dans vite.config.ts :
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
// Simule le DOM navigateur dans Node.js
environment: 'jsdom',
// Charge les matchers jest-dom automatiquement
setupFiles: ['./src/test/setup.ts'],
// Reconnaît les fichiers de test
include: ['**/*.{test,spec}.{ts,tsx}'],
// Couverture de code optionnelle
coverage: {
reporter: ['text', 'html'],
},
},
});
Fichier de setup src/test/setup.ts :
// src/test/setup.ts
// Importe les matchers personnalisés pour tester le DOM HTML
// Ex: toBeInTheDocument(), toHaveValue(), toBeVisible()...
import '@testing-library/jest-dom';
Script npm pour lancer les tests
// package.json
{
"scripts": {
"test": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest --coverage"
}
}
Premiers tests avec render et screen
Les deux outils fondamentaux de RTL sont render() (qui monte le composant)
et screen (qui expose les queries pour trouver les éléments).
Exemple : tester un composant bouton
// src/components/Button.tsx
interface ButtonProps {
label: string;
disabled?: boolean;
onClick: () => void;
}
// Composant simple à tester
export function Button({ label, disabled = false, onClick }: ButtonProps) {
return (
<button onClick={onClick} disabled={disabled} className="btn">
{label}
</button>
);
}
// src/components/Button.test.tsx
import { render, screen } from '@testing-library/react';
import { Button } from './Button';
describe('Button', () => {
it('affiche le label correctement', () => {
// Monte le composant dans un DOM virtuel
render(<Button label="Envoyer" onClick={() => {}} />);
// screen.getByText() cherche un élément par son texte
// Lève une erreur si l'élément n'est pas trouvé
const button = screen.getByText('Envoyer');
// Assertion : le bouton est bien dans le DOM
expect(button).toBeInTheDocument();
});
it('est désactivé quand disabled=true', () => {
render(<Button label="Envoyer" disabled onClick={() => {}} />);
// getByRole() est la query préférée — accessible et sémantique
const button = screen.getByRole('button', { name: 'Envoyer' });
// toBeDisabled() vérifie l'attribut disabled
expect(button).toBeDisabled();
});
});
Les queries RTL par priorité
RTL propose plusieurs types de queries. Il faut les utiliser dans cet ordre de priorité (du plus accessible au moins) :
| Query | Utilisation | Priorité |
|---|---|---|
getByRole |
Rôle ARIA (button, heading, textbox…) | ⭐⭐⭐⭐⭐ Préférée |
getByLabelText |
Champs de formulaire liés à un label | ⭐⭐⭐⭐⭐ Préférée |
getByPlaceholderText |
Input avec placeholder | ⭐⭐⭐ Acceptable |
getByText |
Texte visible dans le DOM | ⭐⭐⭐ Acceptable |
getByTestId |
Attribut data-testid | ⭐ Dernier recours |
Variantes get / query / find
Chaque query existe en trois variantes selon le comportement attendu :
// getBy* : lève une erreur si l'élément n'existe pas
// → À utiliser quand l'élément DOIT être présent
const titre = screen.getByRole('heading', { name: /bienvenue/i });
// queryBy* : retourne null si l'élément n'existe pas
// → À utiliser pour vérifier qu'un élément N'EST PAS là
const erreur = screen.queryByText('Erreur critique');
expect(erreur).not.toBeInTheDocument();
// findBy* : version async, attend que l'élément apparaisse
// → À utiliser pour les éléments qui s'affichent après un délai
const message = await screen.findByText('Chargement terminé');
expect(message).toBeVisible();
getByRole en premier. Si un
élément n'a pas de rôle ARIA naturel, c'est peut-être un problème d'accessibilité dans votre
composant. RTL vous force à écrire du HTML sémantique !
Simuler les interactions utilisateur
Tester qu'un composant s'affiche c'est bien. Tester qu'il répond correctement aux actions
de l'utilisateur, c'est encore mieux. RTL propose deux approches : fireEvent
(bas niveau) et userEvent (recommandé, simule un vrai comportement navigateur).
userEvent vs fireEvent
// ❌ fireEvent : déclenche un seul événement DOM brut
// Pas de focus, pas de frappe clé par clé — trop basique
fireEvent.change(input, { target: { value: 'hello' } });
// ✅ userEvent : simule un vrai utilisateur
// Gère le focus, la saisie clé par clé, les événements intermédiaires
await userEvent.type(input, 'hello');
Exemple : compteur avec clic
// src/components/Counter.tsx
import { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Compteur : {count}</p>
<button onClick={() => setCount(c => c + 1)}>Incrémenter</button>
<button onClick={() => setCount(0)}>Réinitialiser</button>
</div>
);
}
// src/components/Counter.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Counter } from './Counter';
describe('Counter', () => {
it('incrément le compteur au clic', async () => {
// setup() crée une instance userEvent avec gestion du timing
const user = userEvent.setup();
render(<Counter />);
// Récupère le bouton par son texte
const btnIncrement = screen.getByRole('button', { name: 'Incrémenter' });
// Simule un vrai clic utilisateur (async)
await user.click(btnIncrement);
await user.click(btnIncrement);
await user.click(btnIncrement);
// Vérifie l'affichage après 3 clics
expect(screen.getByText('Compteur : 3')).toBeInTheDocument();
});
it('réinitialise le compteur', async () => {
const user = userEvent.setup();
render(<Counter />);
// Incrémente puis réinitialise
await user.click(screen.getByRole('button', { name: 'Incrémenter' }));
await user.click(screen.getByRole('button', { name: 'Incrémenter' }));
await user.click(screen.getByRole('button', { name: 'Réinitialiser' }));
// Doit revenir à 0
expect(screen.getByText('Compteur : 0')).toBeInTheDocument();
});
});
Tester la saisie dans un input
// src/components/SearchBar.tsx
import { useState } from 'react';
interface SearchBarProps {
onSearch: (query: string) => void;
}
export function SearchBar({ onSearch }: SearchBarProps) {
const [query, setQuery] = useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault(); // Empêche le rechargement de page
onSearch(query);
};
return (
<form onSubmit={handleSubmit}>
<label htmlFor="search">Rechercher</label>
<input
id="search"
type="text"
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Tapez votre recherche..."
/>
<button type="submit">Chercher</button>
</form>
);
}
// src/components/SearchBar.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { SearchBar } from './SearchBar';
describe('SearchBar', () => {
it('appelle onSearch avec la valeur saisie', async () => {
const user = userEvent.setup();
// vi.fn() crée une fonction mockée (Vitest) ou jest.fn() avec Jest
const mockOnSearch = vi.fn();
render(<SearchBar onSearch={mockOnSearch} />);
// getByLabelText trouve l'input via son label — priorité accessibilité
const input = screen.getByLabelText('Rechercher');
await user.type(input, 'react hooks');
// Soumet le formulaire en cliquant le bouton
await user.click(screen.getByRole('button', { name: 'Chercher' }));
// Vérifie que la fonction callback a été appelée avec la bonne valeur
expect(mockOnSearch).toHaveBeenCalledTimes(1);
expect(mockOnSearch).toHaveBeenCalledWith('react hooks');
});
});
Tester les hooks avec renderHook
Les hooks personnalisés encapsulent de la logique réutilisable. Pour les tester sans
composant wrapper, RTL fournit renderHook — un utilitaire qui monte un
composant minimal pour exécuter le hook et exposer son résultat.
Exemple : hook useCounter
// src/hooks/useCounter.ts
import { useState, useCallback } from 'react';
interface UseCounterOptions {
initialValue?: number;
min?: number;
max?: number;
}
// Hook personnalisé avec logique de validation min/max
export function useCounter({
initialValue = 0,
min = -Infinity,
max = Infinity,
}: UseCounterOptions = {}) {
const [count, setCount] = useState(initialValue);
// useCallback mémorise les fonctions pour éviter les re-rendus
const increment = useCallback(() => {
setCount(prev => Math.min(prev + 1, max));
}, [max]);
const decrement = useCallback(() => {
setCount(prev => Math.max(prev - 1, min));
}, [min]);
const reset = useCallback(() => {
setCount(initialValue);
}, [initialValue]);
return { count, increment, decrement, reset };
}
// src/hooks/useCounter.test.ts
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';
describe('useCounter', () => {
it('initialise avec la valeur par défaut', () => {
// renderHook monte un composant qui exécute le hook
const { result } = renderHook(() => useCounter());
// result.current donne accès à la valeur retournée par le hook
expect(result.current.count).toBe(0);
});
it('initialise avec une valeur personnalisée', () => {
const { result } = renderHook(() => useCounter({ initialValue: 10 }));
expect(result.current.count).toBe(10);
});
it('incrémente le compteur', () => {
const { result } = renderHook(() => useCounter());
// act() enveloppe les actions qui mettent à jour l'état
// Obligatoire pour que React traite les mises à jour synchroniquement
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
it('respecte la limite max', () => {
const { result } = renderHook(() => useCounter({ initialValue: 5, max: 5 }));
act(() => {
// Essaie d'incrémenter au-delà du max
result.current.increment();
});
// Doit rester à 5 (la limite max)
expect(result.current.count).toBe(5);
});
it('réinitialise à la valeur initiale', () => {
const { result } = renderHook(() => useCounter({ initialValue: 3 }));
act(() => {
result.current.increment();
result.current.increment();
});
expect(result.current.count).toBe(5);
act(() => {
result.current.reset();
});
// Doit revenir à 3 (valeur initiale), pas 0
expect(result.current.count).toBe(3);
});
});
Mocker les appels API avec MSW
Les tests qui font de vraies requêtes réseau sont lents, instables et dépendants d'un serveur. La solution professionnelle est Mock Service Worker (MSW) : il intercepte les requêtes au niveau du Service Worker (en navigateur) ou avec un serveur Node (en test) et retourne des réponses simulées.
Installation MSW
# Installation de Mock Service Worker
npm install --save-dev msw
# Génère le fichier service worker pour le navigateur (optionnel en test)
npx msw init public/
Composant à tester : liste d'utilisateurs
// src/components/UserList.tsx
import { useEffect, useState } from 'react';
interface User {
id: number;
name: string;
email: string;
}
export function UserList() {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetch('/api/users')
.then(res => {
if (!res.ok) throw new Error('Erreur serveur');
return res.json();
})
.then(data => setUsers(data))
.catch(err => setError(err.message))
.finally(() => setLoading(false));
}, []);
if (loading) return <p>Chargement...</p>;
if (error) return <p role="alert">Erreur : {error}</p>;
return (
<ul>
{users.map(user => (
<li key={user.id}>
<strong>{user.name}</strong> — {user.email}
</li>
))}
</ul>
);
}
Configuration MSW pour les tests
// src/test/server.ts
import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';
// Définit les handlers par défaut (réponse nominale)
export const server = setupServer(
http.get('/api/users', () => {
// Retourne une réponse JSON simulée
return HttpResponse.json([
{ id: 1, name: 'Alice Martin', email: 'alice@example.com' },
{ id: 2, name: 'Bob Dupont', email: 'bob@example.com' },
]);
})
);
// src/test/setup.ts (mise à jour)
import '@testing-library/jest-dom';
import { server } from './server';
// Démarre le serveur MSW avant tous les tests
beforeAll(() => server.listen());
// Réinitialise les handlers après chaque test pour isoler les cas
afterEach(() => server.resetHandlers());
// Ferme le serveur après tous les tests
afterAll(() => server.close());
Tests avec et sans erreur
// src/components/UserList.test.tsx
import { render, screen } from '@testing-library/react';
import { server } from '../test/server';
import { http, HttpResponse } from 'msw';
import { UserList } from './UserList';
describe('UserList', () => {
it('affiche la liste des utilisateurs', async () => {
render(<UserList />);
// Pendant le chargement, l'indicateur est visible
expect(screen.getByText('Chargement...')).toBeInTheDocument();
// findBy* attend que l'élément apparaisse (après la requête mock)
expect(await screen.findByText('Alice Martin')).toBeInTheDocument();
expect(screen.getByText('bob@example.com')).toBeInTheDocument();
});
it("affiche une erreur si l'API échoue", async () => {
// Override le handler pour simuler une erreur serveur
server.use(
http.get('/api/users', () => {
return new HttpResponse(null, { status: 500 });
})
);
render(<UserList />);
// Attend que le message d'erreur s'affiche
const erreur = await screen.findByRole('alert');
expect(erreur).toHaveTextContent('Erreur : Erreur serveur');
});
});
Tester les formulaires complexes
Les formulaires sont souvent les parties les plus critiques d'une application et les plus
difficiles à tester manuellement. RTL excelle dans ce domaine grâce aux queries
d'accessibilité (getByLabelText, getByRole) et à
userEvent qui simule une saisie réaliste.
Formulaire d'inscription avec validation
// src/components/RegisterForm.tsx
import { useState } from 'react';
interface FormData {
email: string;
password: string;
confirmPassword: string;
}
interface RegisterFormProps {
onSubmit: (data: FormData) => Promise<void>;
}
export function RegisterForm({ onSubmit }: RegisterFormProps) {
const [form, setForm] = useState<FormData>({
email: '',
password: '',
confirmPassword: '',
});
const [errors, setErrors] = useState<Partial<FormData>>({});
const [submitted, setSubmitted] = useState(false);
// Validation côté client
const validate = (data: FormData): Partial<FormData> => {
const errs: Partial<FormData> = {};
if (!data.email.includes('@')) errs.email = 'Email invalide';
if (data.password.length < 8) errs.password = '8 caractères minimum';
if (data.password !== data.confirmPassword) {
errs.confirmPassword = 'Les mots de passe ne correspondent pas';
}
return errs;
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setForm(prev => ({ ...prev, [e.target.name]: e.target.value }));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const errs = validate(form);
if (Object.keys(errs).length > 0) {
setErrors(errs);
return;
}
await onSubmit(form);
setSubmitted(true);
};
if (submitted) return <p>Inscription réussie !</p>;
return (
<form onSubmit={handleSubmit} noValidate>
<div>
<label htmlFor="email">Email</label>
<input id="email" name="email" type="email"
value={form.email} onChange={handleChange} />
{errors.email && <span role="alert">{errors.email}</span>}
</div>
<div>
<label htmlFor="password">Mot de passe</label>
<input id="password" name="password" type="password"
value={form.password} onChange={handleChange} />
{errors.password && <span role="alert">{errors.password}</span>}
</div>
<div>
<label htmlFor="confirmPassword">Confirmer le mot de passe</label>
<input id="confirmPassword" name="confirmPassword" type="password"
value={form.confirmPassword} onChange={handleChange} />
{errors.confirmPassword && <span role="alert">{errors.confirmPassword}</span>}
</div>
<button type="submit">S'inscrire</button>
</form>
);
}
// src/components/RegisterForm.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { RegisterForm } from './RegisterForm';
// Fonction utilitaire pour remplir le formulaire
async function fillForm(user: ReturnType<typeof userEvent.setup>, overrides = {}) {
const defaults = {
email: 'alice@example.com',
password: 'motdepasse123',
confirmPassword: 'motdepasse123',
};
const values = { ...defaults, ...overrides };
await user.type(screen.getByLabelText('Email'), values.email);
await user.type(screen.getByLabelText('Mot de passe'), values.password);
await user.type(screen.getByLabelText('Confirmer le mot de passe'), values.confirmPassword);
}
describe('RegisterForm', () => {
it('soumet les données valides et affiche la confirmation', async () => {
const user = userEvent.setup();
const mockSubmit = vi.fn().mockResolvedValue(undefined); // Simule async
render(<RegisterForm onSubmit={mockSubmit} />);
await fillForm(user);
await user.click(screen.getByRole('button', { name: "S'inscrire" }));
// Vérifie que onSubmit a reçu les bonnes données
expect(mockSubmit).toHaveBeenCalledWith({
email: 'alice@example.com',
password: 'motdepasse123',
confirmPassword: 'motdepasse123',
});
// Message de succès affiché
expect(await screen.findByText('Inscription réussie !')).toBeInTheDocument();
});
it("affiche une erreur si l'email est invalide", async () => {
const user = userEvent.setup();
render(<RegisterForm onSubmit={vi.fn()} />);
// Saisit un email invalide (sans @)
await user.type(screen.getByLabelText('Email'), 'emailinvalide');
await user.type(screen.getByLabelText('Mot de passe'), 'motdepasse123');
await user.type(screen.getByLabelText('Confirmer le mot de passe'), 'motdepasse123');
await user.click(screen.getByRole('button', { name: "S'inscrire" }));
// Cherche le message d'erreur (role="alert" dans le JSX)
const alertes = screen.getAllByRole('alert');
expect(alertes[0]).toHaveTextContent('Email invalide');
});
it("affiche une erreur si les mots de passe ne correspondent pas", async () => {
const user = userEvent.setup();
render(<RegisterForm onSubmit={vi.fn()} />);
await fillForm(user, { confirmPassword: 'different123' });
await user.click(screen.getByRole('button', { name: "S'inscrire" }));
expect(
screen.getByText('Les mots de passe ne correspondent pas')
).toBeInTheDocument();
});
});
Bonnes pratiques et pièges à éviter
1. Ne pas tester les détails d'implémentation
// ❌ MAUVAIS : teste l'état interne (détail d'implémentation)
// Si vous renommez le state, ce test casse sans raison valable
const { result } = renderHook(() => useToggle());
expect(result.current.isOpen).toBe(false); // Accède à la variable interne
// ✅ BON : teste le comportement visible par l'utilisateur
render(<Modal />);
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
await user.click(screen.getByRole('button', { name: 'Ouvrir' }));
expect(screen.getByRole('dialog')).toBeVisible();
2. Un test = un comportement précis
// ❌ MAUVAIS : trop de choses dans un seul test — si ça échoue, pourquoi ?
it('le formulaire fonctionne', async () => {
// Teste la validation ET la soumission ET la confirmation EN MÊME TEMPS
});
// ✅ BON : chaque test vérifie une seule chose
it("affiche l'erreur si l'email est vide", async () => { /* ... */ });
it('soumet avec des données valides', async () => { /* ... */ });
it('désactive le bouton pendant la soumission', async () => { /* ... */ });
3. Éviter les data-testid quand possible
// ❌ MAUVAIS : data-testid couplé à l'implémentation, pas à l'usage
<button data-testid="submit-btn">Envoyer</button>
screen.getByTestId('submit-btn');
// ✅ BON : query par rôle — test accessible ET robuste
<button type="submit">Envoyer</button>
screen.getByRole('button', { name: 'Envoyer' });
4. Wrapper avec les providers nécessaires
// Si votre composant utilise un Context ou un Router, il faut les wrapper
// Créez une fonction render personnalisée pour éviter la répétition :
// src/test/utils.tsx
import { render, RenderOptions } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import { AuthProvider } from '../contexts/AuthContext';
// Wrapper global avec tous les providers
function AllProviders({ children }: { children: React.ReactNode }) {
return (
<BrowserRouter>
<AuthProvider>
{children}
</AuthProvider>
</BrowserRouter>
);
}
// Re-exporte render avec wrapper automatique
export function renderWithProviders(ui: React.ReactElement, options?: RenderOptions) {
return render(ui, { wrapper: AllProviders, ...options });
}
// Utilisation dans les tests :
// import { renderWithProviders } from '../test/utils';
// renderWithProviders(<MonComposant />);
5. Tester l'accessibilité de base
// @axe-core/react permet de détecter automatiquement les violations ARIA
// npm install --save-dev @axe-core/react jest-axe
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
it('respecte les règles ARIA de base', async () => {
const { container } = render(<RegisterForm onSubmit={vi.fn()} />);
const results = await axe(container);
// Lance une erreur si le composant viole des règles d'accessibilité
expect(results).toHaveNoViolations();
});
Checklist qualité des tests React
Avant de merger une PR, passez cette checklist pour vous assurer que vos tests sont solides, maintenables et apportent une vraie valeur :
Configuration et setup
- Vitest ou Jest configuré avec jsdom et @testing-library/jest-dom
- Fichier
setup.tsqui importe jest-dom - MSW configuré pour intercepter les requêtes réseau
- Fonction
renderWithProviderscréée si contexts nécessaires - Script
npm testfonctionnel en CI
Qualité des tests
- Chaque test vérifie un seul comportement précis
- Queries par rôle ou label (pas data-testid)
- userEvent utilisé à la place de fireEvent
- Tests asynchrones avec await et findBy*
- Pas d'accès à l'état interne des composants
- Mocks réinitialisés entre chaque test (afterEach)
Couverture des scénarios
- Cas nominal (happy path) testé
- Cas d'erreur (API failure, validation) testé
- États intermédiaires (loading, disabled) testés
- Interactions clavier testées si pertinent
- Hooks complexes testés avec renderHook
npm run test:coverage pour visualiser les zones
non testées.
Récapitulatif : stack de test recommandée
| Rôle | Outil | Version | Raison |
|---|---|---|---|
| Test runner | Vitest | 2.x | Rapide, compatible Vite, API Jest |
| Render React | @testing-library/react | 16.x | Standard de l'industrie |
| Interactions | @testing-library/user-event | 14.x | Simulation réaliste utilisateur |
| Matchers DOM | @testing-library/jest-dom | 6.x | Assertions HTML expressives |
| Mock API | MSW | 2.x | Interception réseau réaliste |
| Accessibilité | jest-axe | 9.x | Détection violations ARIA |
React Testing Library a changé la façon dont l'industrie pense aux tests frontend. En forçant à tester depuis la perspective de l'utilisateur plutôt que de l'implémentation, elle produit des suites de tests plus robustes, plus maintenables, et qui donnent une vraie confiance lors des refactorisations. Commencez par les composants critiques — formulaires, composants de navigation — et étendez progressivement votre couverture.