
Jusqu'à présent, nous avons testé des fonctions JavaScript simples. Mais qu'en est-il de nos composants React ? Comment vérifier que ce qui est affiché fonctionne correctement ?
React Testing Library est LA bibliothèque recommandée pour tester les composants React. Sa philosophie : tester les composants comme un utilisateur les utiliserait, sans se soucier des détails d'implémentation internes.
npm install -D @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom
Mais attendez... pourquoi tous ces packages ? Comprenons ce que chacun fait :
@testing-library/react: Les outils pour tester les composants React (render, screen, etc.)
@testing-library/jest-dom: Des matchers supplémentaires pour tester le DOM (toBeInTheDocument, toHaveStyle, etc.)
@testing-library/user-event: Simule les interactions utilisateur de façon réaliste
jsdom: C'est crucial ! Simule un navigateur en Node.js
Nos tests s'exécutent dans Node.js, pas dans un vrai navigateur. Or, React a besoin du DOM (Document Object Model) pour fonctionner.
Sans jsdom, Node.js n'a pas accès àwindow,document, etc., et React crash. Avec jsdom, ces objets sont simulés et React fonctionne normalement.
C'est pour ça qu'on doit le configurer dans Vitest.
Créez un fichier vitest.config.js à la racine du projet :
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: './src/test/setup.js',
},
})
Explications ligne par ligne :
1.plugins: [react()]
Sans ça, Vitest ne comprend pas le JSX. Il utilise le même plugin que votrevite.config.js.
2.globals: true
Vous pouvez écriredescribe()directement sansimport { describe } from 'vitest'. C'est optionnel, mais pratique (comme Jest).
3.environment: 'jsdom'
C'est le plus important ! Active la simulation du navigateur. Sans ça, vous aurez l'erreur :ReferenceError: document is not defined
4.setupFiles: './src/test/setup.js'
Fichier exécuté avant chaque fichier de test. Pour configurer des choses globales (on va le créer maintenant).
Maintenant, créonssrc/test/setup.js. Mais d'abord, pourquoi en avons-nous besoin ?
Par défaut, Vitest connaîttoBe(),toEqual(), etc. Mais il ne connaît pas d'autres matchers comme toBeInTheDocument() ou toHaveStyle()ces matcheurs viennent de @testing-library/jest-dom et doivent être enregistrés.
Entre chaque test, il faut nettoyer le DOM simulé, sinon le premier test pourrait laisser des éléments qui perturbent le second test.
Le fichier src./test/setup.js va nous aider à régler tous ces problèmes
import { expect, afterEach } from 'vitest'
import { cleanup } from '@testing-library/react'
import * as matchers from '@testing-library/jest-dom/matchers'
expect.extend(matchers)
afterEach(() => {
cleanup()
})Les éléments clés ici se trouve à la ligne 5 avec l'ajout des nouveaux matchers et les lignes 7 à 9 avec la configuration du nettoyage entre chaque test
Créons un composant Button dans src/components/Button/Button.jsx :
export function Button({ children, onClick, variant = 'primary' }) {
const baseStyle = {
padding: '10px 20px',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '16px',
}
const variants = {
primary: {
backgroundColor: '#007bff',
color: 'white',
},
secondary: {
backgroundColor: '#6c757d',
color: 'white',
},
danger: {
backgroundColor: '#dc3545',
color: 'white',
},
}
return (
<button
style={{ ...baseStyle, ...variants[variant] }}
onClick={onClick}
>
{children}
</button>
)
}Créez src/components/Button/Button.test.jsx :
import { describe, it, expect } from 'vitest'
import { render, screen } from '@testing-library/react'
import { Button } from './Button'
describe('Button', () => {
it('should render without crashing', () => {
render(<Button>Click me</Button>)
const button = screen.getByRole('button')
expect(button).toBeInTheDocument()
})
it('should display the correct text', () => {
render(<Button>Mon bouton</Button>)
expect(screen.getByText('Mon bouton')).toBeInTheDocument()
})
it('should apply the primary variant by default', () => {
render(<Button>Primary</Button>)
const button = screen.getByRole('button')
expect(button).toHaveStyle({ backgroundColor: '#007bff' })
})
it('should apply the danger variant when specified', () => {
render(<Button variant="danger">Delete</Button>)
const button = screen.getByRole('button')
expect(button).toHaveStyle({ backgroundColor: '#dc3545' })
})
})React Testing Library propose plusieurs façons de sélectionner des éléments, par ordre de priorité :
getByRole: À privilégier (accessible et sémantique)
getByLabelText: Pour les champs de formulaire
getByPlaceholderText: Pour les inputs avec placeholder
getByText: Pour le contenu textuel
getByTestId: En dernier recours uniquement
// Préféré : par rôle
screen.getByRole('button')
screen.getByRole('button', { name: 'Submit' })
screen.getByRole('textbox')
// Par label (formulaires)
screen.getByLabelText('Email')
// Par texte
screen.getByText('Welcome')
screen.getByText(/hello/i) // insensible à la casse
// En dernier recours
screen.getByTestId('custom-element')Chaque sélecteur existe en plusieurs variantes:
getBy...: Lance une erreur si l'élément n'existe pas. Utilisez-le quand vous êtes certain que l'élément doit être présent.
queryBy...: Retournenullsi l'élément n'existe pas. Utilisez-le pour vérifier qu'un élément n'existe pas.
findBy...: Asynchrone, attend que l'élément apparaisse. Utilisez-le pour attendre qu'un élément apparaisse (après un chargement, par exemple).
En plus de pouvoir vérifier si les éléments existent bien sur notre page, nous pouvons également tester la réaction de notre composant aux différents évènements et en particulier ici aux intéractions utilisateurs. Pour nous importerons le userEvents
Créons un composant Counter dans src/components/Counter/Counter.jsx :
import { useState } from 'react'
export function Counter({ initialValue = 0, step = 1 }) {
const [count, setCount] = useState(initialValue)
return (
<div>
<h2>Compteur : {count}</h2>
<button onClick={() => setCount(count + step)}>
Incrémenter
</button>
<button onClick={() => setCount(count - step)}>
Décrémenter
</button>
<button onClick={() => setCount(initialValue)}>
Réinitialiser
</button>
</div>
)
}Créez src/components/Counter/Counter.test.jsx :
import { describe, it, expect } from 'vitest'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { Counter } from './Counter'
describe('Counter', () => {
it('should display the initial value', () => {
render(<Counter initialValue={5} />)
expect(screen.getByText('Compteur : 5')).toBeInTheDocument()
})
it('should increment the counter when clicking increment button', async () => {
const user = userEvent.setup()
render(<Counter />)
const incrementButton = screen.getByRole('button', { name: 'Incrémenter' })
await user.click(incrementButton)
expect(screen.getByText('Compteur : 1')).toBeInTheDocument()
await user.click(incrementButton)
expect(screen.getByText('Compteur : 2')).toBeInTheDocument()
})
it('should decrement the counter when clicking decrement button', async () => {
const user = userEvent.setup()
render(<Counter initialValue={5} />)
const decrementButton = screen.getByRole('button', { name: 'Décrémenter' })
await user.click(decrementButton)
expect(screen.getByText('Compteur : 4')).toBeInTheDocument()
})
it('should reset to initial value when clicking reset button', async () => {
const user = userEvent.setup()
render(<Counter initialValue={10} />)
// Incrémenter plusieurs fois
const incrementButton = screen.getByRole('button', { name: 'Incrémenter' })
await user.click(incrementButton)
await user.click(incrementButton)
expect(screen.getByText('Compteur : 12')).toBeInTheDocument()
// Réinitialiser
const resetButton = screen.getByRole('button', { name: 'Réinitialiser' })
await user.click(resetButton)
expect(screen.getByText('Compteur : 10')).toBeInTheDocument()
})
it('should use custom step value', async () => {
const user = userEvent.setup()
render(<Counter step={5} />)
const incrementButton = screen.getByRole('button', { name: 'Incrémenter' })
await user.click(incrementButton)
expect(screen.getByText('Compteur : 5')).toBeInTheDocument()
})
})// ❌ Mauvais : fireEvent
import { fireEvent } from '@testing-library/react'
fireEvent.click(button)// ✅ Bon : userEvent (simule mieux le comportement utilisateur)
import userEvent from '@testing-library/user-event'
const user = userEvent.setup()
await user.click(button)userEventsimule les événements de manière plus réaliste. Par exemple, un click déclenche hover, focus, mousedown, mouseup, puis click. AvecfireEvent, seul l'événement click est déclenché, sans le contexte complet.
Comprenez le problème
Jusqu'à présent, nous avons testé des composants autonomes (Button, Counter). Mais dans une vraie application React, les composants communiquent entre eux via des props de type fonction (callbacks). Prenons un exemple courant:
function ParentComponent() {
const handleLogin = (credentials) => {
// Envoie les données à l'API
fetch('/api/login', {
method: 'POST',
body: JSON.stringify(credentials)
})
}
return <LoginForm onSubmit={handleLogin} />
}Question : Comment tester queLoginFormappelle correctementonSubmitavec les bonnes données?
Problème : Nous ne voulons PAS vraiment envoyer une requête à l'API dans nos tests !
C'est là qu'interviennent les fonctions mock. Une fonction mock est une fausse fonction qui :
Ne fait rien (ou fait ce qu'on lui dit)
Enregistre tous les appels qu'elle reçoit
Permet de vérifier comment elle a été appelée
Pourquoi c'est utile pour tester React ?
Dans nos tests, on peut remplacer les vraies fonctions par des mocks :
// ❌ Avant : on passerait une vraie fonction
function realSubmit(data) {
fetch('/api/login', { body: JSON.stringify(data) })
}
render(<LoginForm onSubmit={realSubmit} />)
// ✅ Avec mock : on contrôle tout
const mockSubmit = vi.fn()
render(<LoginForm onSubmit={mockSubmit} />)
// Maintenant on peut vérifier que LoginForm l'appelle correctement
expect(mockSubmit).toHaveBeenCalledWith({
email: 'test@test.com',
password: 'secret'
})Voici les vérifications les plus courantes :
1. Vérifier qu'une fonction a été appelée
expect(mockFn).toHaveBeenCalled()
expect(mockFn).not.toHaveBeenCalled()2. Vérifier le nombre d'appels
expect(mockFn).toHaveBeenCalledTimes(1)
expect(mockFn).toHaveBeenCalledTimes(3)3. Vérifier les arguments d'un appel
// N'importe quel appel avec ces arguments
expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2')
// Le premier appel
expect(mockFn).toHaveBeenNthCalledWith(1, 'first call')
// Le dernier appel
expect(mockFn).toHaveBeenLastCalledWith('last call')4. Vérifier la structure des arguments (sans valeurs exactes)
expect(mockFn).toHaveBeenCalledWith(
expect.objectContaining({
email: expect.any(String),
password: expect.any(String)
})
)Si vous souhaitez voir un exemple complet rendez vous sur la branche P1C3-Login où vous trouverez le test d'un formulaire de connexion.
Vous avez créé un hook personnalisé. Comment le tester ? Première intuition : l'appeler directement dans un test. Cependant si vous vous rappelez la base des hooks React, ils ont une règle fondamentale : ils ne peuvent être appelés QUE dans un composant React. C'est une limitation de React lui-même, pas de nos outils de test !
La solution : tester via un composant wrapper
Puisqu'on ne peut pas appeler un hook directement, on va créer un composant de test temporaire qui utilise ce hook :
// Composant créé uniquement pour tester le hook
function TestComponent() {
const { value, toggle } = useToggle()
return (
<div>
<p>Status: {value ? 'ON' : 'OFF'}</p>
<button onClick={toggle}>Toggle</button>
</div>
)
}
// Maintenant on peut le tester comme un composant normal
it('should toggle', async () => {
render(<TestComponent />)
await user.click(screen.getByRole('button'))
expect(screen.getByText('Status: ON')).toBeInTheDocument()
})L'idée clé : On encapsule le hook dans un composant minimal qui expose son comportement via le DOM.
Lorsque vous tests un hook vous pouvez garder en tête ces bonnes pratiques :
Exposez toutes les valeurs et fonction du hook
Testez le comportement du hook pas son implémentation
Si le hook a des effets de bord, nettoyez entre les tests
Vous trouverez un exemple avec le hook useToogle dans src/hooks/toogle sur la branche P1C3-Begin

Dans le dossier components de la branche P1C3-Begin vous trouverez un composant TodoList qui permet :
D'ajouter une tâche
De cocher une tâche comme terminée
De supprimer une tâche
D'afficher le nombre de tâches actives
Écrivez les tests pour :
Vérifier qu'on peut ajouter une tâche
Vérifier qu'on peut cocher une tâche
Vérifier qu'on peut supprimer une tâche
Vérifier que le compteur se met à jour correctement
React Testing Library teste les composants comme un utilisateur les utiliserait
PrivilégiezgetByRolepour sélectionner les éléments
UtilisezuserEvent(pasfireEvent) pour simuler les interactions
Testez les hooks via des composants qui les utilisent
Ne testez pas les détails d'implémentation, testez le comportement visible
Dans le prochain chapitre, nous allons apprendre à simuler des appels API dans nos tests !