Testez vos composants avec React Testing Library

Découvrez React Testing Library

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.

Installez React Testing Library

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

Pourquoi avons-nous besoin de jsdom ?

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.

Configurez Vitest pour tester React

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).

Créez le fichier de setup

Maintenant, créonssrc/test/setup.js. Mais d'abord, pourquoi en avons-nous besoin ?

Problème 1 : Les matchers personnalisés

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. 

Problème 2 : Le nettoyage entre les tests

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éez votre premier composant testé

Créez un composant simple

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>
  )
}

Testez le rendu du composant

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' })
  })
})

Comprenez les sélecteurs de React Testing Library

React Testing Library propose plusieurs façons de sélectionner des éléments, par ordre de priorité :

  1. getByRole: À privilégier (accessible et sémantique)

  2. getByLabelText: Pour les champs de formulaire

  3. getByPlaceholderText: Pour les inputs avec placeholder

  4. getByText: Pour le contenu textuel

  5. 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).

Testez les interactions utilisateur

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éez un compteur interactif

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>
  )
}

Testez les interactions

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.

Testez les callbacks avec les fonctions mock

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 !

Découvrez les fonctions mock avec vi.fn()

C'est là qu'interviennent les fonctions mock. Une fonction mock est une fausse fonction qui :

  1. Ne fait rien (ou fait ce qu'on lui dit)

  2. Enregistre tous les appels qu'elle reçoit

  3. 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' 
})
Découvrez les principales assertions disponibles pour les mocks

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.

Testez les hooks personnalisés

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 : 

  1. Exposez toutes les valeurs et fonction du hook

  2. Testez le comportement du hook pas son implémentation

  3. 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

À vous de jouer !

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 :

  1. Vérifier qu'on peut ajouter une tâche

  2. Vérifier qu'on peut cocher une tâche

  3. Vérifier qu'on peut supprimer une tâche

  4. Vérifier que le compteur se met à jour correctement

En résumé

  • 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 !

Et si vous obteniez un diplôme OpenClassrooms ?
  • Formations jusqu’à 100 % financées
  • Date de début flexible
  • Projets professionnalisants
  • Mentorat individuel
Trouvez la formation et le financement faits pour vous