Testing React avec Vitest

Philosophie, configuration et premiers tests

React Testing Library + Vitest + userEvent

Objectifs de la leçon

1. Comprendre la philosophie

Testing Library : tester le comportement utilisateur

2. Configurer l'environnement

Vitest + jsdom + React Testing Library

3. Maîtriser les queries

getBy, queryBy, findBy — quand utiliser laquelle

4. Simuler les interactions

userEvent pour des tests réalistes

Plan du cours

1. Pourquoi tester ?

Le coût d'un bug en production vs le coût d'un test

2. Testing Library

Tester le COMPORTEMENT, pas l'implémentation

3. Setup complet

Vitest + jsdom + @testing-library/react

4. render, screen, queries

Les 3 types de queries et leurs différences

5. Live Coding

Tester un Button, puis un formulaire avec userEvent

Module 1

Pourquoi tester ?

Le coût d'un bug en production vs le coût d'un test

Le coût d'un bug

Plus un bug est détecté tard, plus il coûte cher

Phase dev

1x

Correction immédiate

QA

5x

Retour en arrière

Staging

15x

Processus de release

Production

100x

Hotfix + réputation

Les tests vous font gagner du temps

Sans tests

  • • Peur de casser quelque chose
  • • Tests manuels rĂ©pĂ©titifs
  • • RĂ©gressions frĂ©quentes
  • • Refactoring risquĂ©

Avec tests

  • • Confiance dans les changements
  • • DĂ©tection immĂ©diate des bugs
  • • Documentation vivante
  • • Refactoring serein

Module 2

Testing Library Philosophy

Tester le COMPORTEMENT, pas l'implémentation

Le principe fondamental

"The more your tests resemble the way your software is used,

the more confidence they can give you."

— Kent C. Dodds, créateur de Testing Library

Tester comme un UTILISATEUR

L'utilisateur ne connaît pas vos className, useState ou testId

âś— Mauvais tests

getByTestId('submit-btn')

container.querySelector('.btn')

expect(state.called).toBe(true)

Teste l'implémentation

âś“ Bons tests

getByRole('button', {name: /envoyer/i})

getByText('Connexion')

getByLabelText('Email')

Teste ce que l'utilisateur voit

Les priorités de queries

Testing Library recommande cet ordre

1 getByRole() Accessibilité maximale
2 getByLabelText() Formulaires
3 getByText() Contenu textuel
... getByTestId() Dernier recours seulement

Module 3

Setup Vitest + React Testing Library

Configuration complète dans un projet Vite

Installation des packages

npm install -D vitest @vitest/ui jsdom

npm install -D @testing-library/react

npm install -D @testing-library/user-event

npm install -D @testing-library/jest-dom

vitest

Test runner ultra-rapide

jsdom

Simulation du DOM

@testing-library/react

render, screen, queries

user-event

Interactions réalistes

vitest.config.ts

import { defineConfig } from 'vitest/config'

import react from '@vitejs/plugin-react'

export default defineConfig({

plugins: [react()],

test: {

environment: 'jsdom',

globals: true,

setupFiles: './src/setupTests.ts'

}

})

src/setupTests.ts

import '@testing-library/jest-dom'

Matchers supplémentaires disponibles :

  • • toBeInTheDocument()
  • • toHaveTextContent()
  • • toBeVisible()
  • • toHaveClass()

package.json — scripts

{

"scripts": {

"test": "vitest",

"test:ui": "vitest --ui",

"test:coverage": "vitest --coverage"

}

}

Module 4

render, screen, queries

Les outils fondamentaux de React Testing Library

render() — Monter le composant

Affiche votre composant dans le DOM virtuel

import { render } from '@testing-library/react'

import Button from './Button'

// Dans le test

render(<Button>Cliquez-moi</Button>)

screen — Accéder au DOM

Objet global contenant toutes les queries

import { screen } from '@testing-library/react'

// Après render()

const button = screen.getByRole('button', { name: /cliquez/i })

screen expose toutes les queries :

screen.getByText(), screen.getByRole(), screen.getByLabelText()...

Les 3 types de queries

Query Si trouvé Si non trouvé Usage
getBy* Retourne l'élément Throw Error Élément présent
queryBy* Retourne l'élément Retourne null Vérifier absence
findBy* Promise (élément) Promise reject Async (attente)

getBy — L'élément DOIT exister

// ✅ Bon usage : vérifier présence

expect(screen.getByText('Bienvenue')).toBeInTheDocument()

// ❌ Si l'élément n'existe pas → Error!

Utilisez getBy quand :

Vous vous attendez à ce que l'élément soit présent immédiatement

queryBy — Vérifier l'absence

// ✅ Bon usage : vérifier absence

expect(screen.queryByText('Erreur')).not.toBeInTheDocument()

// ✅ Retourne null si non trouvé (pas d'erreur)

Utilisez queryBy quand :

Vous voulez vérifier qu'un élément n'est PAS dans le document

findBy — Attendre l'async

// âś… Bon usage : attendre apparition

const message = await screen.findByText('Chargement terminé')

expect(message).toBeInTheDocument()

Utilisez findBy quand :

L'élément apparaît après un délai (API, animation, setTimeout)

Module 5

Live Coding

Tester un Button, puis un formulaire

Test 1 : Composant Button

Button.tsx

export function Button({ children, onClick }) {

return (

<button onClick={onClick}>

{children}

</button>

)

}

Button.test.tsx

import { render, screen } from '@testing-library/react'

// ... imports

it('affiche le texte', () => {

render(<Button>Cliquez</Button>)

expect(screen.getByRole('button')).toHaveTextContent('Cliquez')

})

userEvent — Interactions réalistes

Simuler les actions de l'utilisateur

import userEvent from '@testing-library/user-event'

// Setup dans chaque test

it('appelle onClick au clic', async () => {

const handleClick = vi.fn()

render(<Button onClick={handleClick}>Ok</Button>)

const user = userEvent.setup()

await user.click(screen.getByRole('button'))

expect(handleClick).toHaveBeenCalledTimes(1)

})

userEvent vs fireEvent

âś— fireEvent

fireEvent.click(element)

  • • ÉvĂ©nement synthĂ©tique
  • • Ne simule pas tout le workflow
  • • Moins rĂ©aliste

âś“ userEvent

await user.click(element)

  • • Simule l'interaction complète
  • • Focus, blur, events en cascade
  • • Plus proche du comportement rĂ©el

Test 2 : Formulaire de connexion

it('soumet le formulaire avec les valeurs', async () => {

const user = userEvent.setup()

render(<LoginForm />)

// Remplir les champs

await user.type(screen.getByLabelText('Email'), '[email protected]')

await user.type(screen.getByLabelText('Mot de passe'), 'secret123')

// Soumettre

await user.click(screen.getByRole('button', {name: /connexion/i}))

})

Pièges courants

Les erreurs à éviter

Piège #1 : Tester l'implémentation

âś— MAUVAIS

expect(component.state.count)

.toBe(5)

Teste les détails internes

âś“ CORRECT

expect(screen.getByText('5'))

.toBeInTheDocument()

Teste le RÉSULTAT visible

Piège #2 : Abuser de testId

✗ ÉVITER

screen.getByTestId('btn-submit')

screen.getByTestId('input-email')

screen.getByTestId('error-msg')

testId = dernier recours

✓ PRÉFÉRER

screen.getByRole('button',

{name: /envoyer/i})

screen.getByLabelText('Email')

Queries accessibles

Piège #3 : Oublier await

âś— ERREUR

user.click(button)

// Pas de await → test flaky

userEvent est ASYNC !

âś“ CORRECT

await user.click(button)

Toujours await userEvent

Piège #4 : Mauvaise query pour absence

âś— ERREUR

expect(

screen.getByText('Erreur')

).not.toBeInTheDocument()

// Throw si pas trouvé!

âś“ CORRECT

expect(

screen.queryByText('Erreur')

).not.toBeInTheDocument()

// Retourne null si absent

Points clés à retenir

Comportement

Testez ce que l'utilisateur VOIT, pas le code interne

Queries

getBy = présent, queryBy = absent, findBy = async

userEvent

Toujours await, plus réaliste que fireEvent

Accessibilité

getByRole > getByText > getByTestId

Ă€ retenir !

  • âś“ Testez le rĂ©sultat visible, pas les dĂ©tails d'implĂ©mentation
  • âś“ Utilisez getByRole et getByLabelText en prioritĂ©
  • âś“ userEvent + await pour les interactions
  • âś“ queryBy pour vĂ©rifier l'absence, findBy pour l'async

C'est terminé !

Vous savez maintenant tester vos composants React

Questions ?