Philosophie, configuration et premiers tests
React Testing Library + Vitest + userEvent
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
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
Pourquoi tester ?
Le coût d'un bug en production vs le coût d'un test
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
Sans tests
Avec tests
Testing Library Philosophy
Tester le COMPORTEMENT, pas l'implémentation
"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
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
Testing Library recommande cet ordre
Setup Vitest + React Testing Library
Configuration complète dans un projet Vite
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
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'
}
})
import '@testing-library/jest-dom'
Matchers supplémentaires disponibles :
{
"scripts": {
"test": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest --coverage"
}
}
render, screen, queries
Les outils fondamentaux de React Testing Library
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>)
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()...
| 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) |
// ✅ 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
// ✅ 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
// ✅ 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)
Live Coding
Tester un Button, puis un formulaire
Button.tsx
interface ButtonProps {
children: React.ReactNode;
onClick?: () => void;
disabled?: boolean;
}
export function Button({ children, onClick, disabled = false }: ButtonProps) {
return (
<button
onClick={onClick}
disabled={disabled}
className="px-4 py-2 bg-blue-500 text-white rounded"
>
{children}
</button>
)
}
Button.test.tsx
import { render, screen } from '@testing-library/react'
import { test, expect } from 'vitest'
import userEvent from '@testing-library/user-event'
import { Button } from './Button'
test('affiche le texte passé en props', () => {
render(<Button>Cliquez-moi</Button>)
const button = screen.getByRole('button', { name: 'Cliquez-moi' })
expect(button).toBeInTheDocument()
})
Simuler les actions de l'utilisateur
import { render, screen } from '@testing-library/react'
import { test, expect, vi } from 'vitest'
import userEvent from '@testing-library/user-event'
import { Button } from './Button'
test('appelle onClick au clic', async () => {
const handleClick = vi.fn()
render(<Button onClick={handleClick}>Cliquez-moi</Button>)
const user = userEvent.setup()
await user.click(screen.getByRole('button'))
expect(handleClick).toHaveBeenCalledTimes(1)
expect(handleClick).toHaveBeenCalledWith()
})
✅ userEvent simule le comportement réel : focus → click → blur
Couvrir tous les cas d'usage
test('est désactivé quand disabled=true', () => {
render(<Button disabled>Disabled</Button>)
const button = screen.getByRole('button')
expect(button).toBeDisabled()
})
test('n\'appelle pas onClick quand désactivé', async () => {
const handleClick = vi.fn()
render(<Button onClick={handleClick} disabled>Click</Button>)
const user = userEvent.setup()
await user.click(screen.getByRole('button'))
expect(handleClick).not.toHaveBeenCalled()
})
test('a les classes CSS appropriées', () => {
render(<Button>Styled</Button>)
const button = screen.getByRole('button')
expect(button).toHaveClass('px-4', 'py-2', 'bg-blue-500')
})
✗ fireEvent
fireEvent.click(element)
✓ userEvent
await user.click(element)
LoginForm.tsx
export function LoginForm({ onSubmit }) {
return (
<form onSubmit={onSubmit}>
<label>
Email:
<input name="email" type="email" />
</label>
<label>
Mot de passe:
<input name="password" type="password" />
</label>
<button type="submit">Connexion</button>
</form>
)
}
LoginForm.test.tsx
import { render, screen } from '@testing-library/react'
import { test, expect, vi } from 'vitest'
import userEvent from '@testing-library/user-event'
import { LoginForm } from './LoginForm'
test('soumet le formulaire avec les valeurs', async () => {
const handleSubmit = vi.fn()
render(<LoginForm onSubmit={handleSubmit} />)
const user = userEvent.setup()
// Remplir les champs
await user.type(screen.getByLabelText('Email:'), 'test@example.com')
await user.type(screen.getByLabelText('Mot de passe:'), 'secret123')
// Soumettre
await user.click(screen.getByRole('button', {name: 'Connexion'}))
expect(handleSubmit).toHaveBeenCalledWith(expect.any(Object), {
email: 'test@example.com',
password: 'secret123'
})
})
Organisation des tests en architecture Feature
Où placer vos tests dans une architecture feature-based
Dans une structure classique, les tests sont loin du code
// ❌ Structure classique avec tests séparés
📁 src/
📁 components/
📄 Button.tsx
📄 UserCard.tsx
📁 hooks/
📄 useUsers.ts
📁 services/
📄 userService.ts
📁 tests/
📄 Button.test.tsx
📄 UserCard.test.tsx
📄 useUsers.test.ts
📄 userService.test.ts
⚠️ Difficile de trouver les tests d'une feature spécifique
Placer les tests à côté du code qu'ils testent
// ✅ Feature-based avec tests co-localisés
📁 features/
📁 users/
📁 components/
📄 UserCard.tsx
📄 UserCard.test.tsx // ← à côté!
📄 UserForm.tsx
📄 UserForm.test.tsx // ← à côté!
📁 hooks/
📄 useUsers.ts
📄 useUsers.test.ts // ← à côté!
📁 services/
📄 userService.ts
📄 userService.test.ts // ← à côté!
📄 types.ts
✅ Tests et code dans le même dossier = facile à trouver et maintenir
🎯 Proximité
🚀 Maintenance
📋 Organisation
⚡ Performance
📁 features/users/
📁 components/
│ 📄 UserCard.tsx
│ 📄 UserCard.test.tsx
│ 📄 UserList.tsx
│ 📄 UserList.test.tsx
│ 📄 UserForm.tsx
│ 📄 UserForm.test.tsx
📁 hooks/
│ 📄 useUsers.ts
│ 📄 useUsers.test.ts
📁 services/
│ 📄 userService.ts
│ 📄 userService.test.ts
📁 utils/
│ 📄 userHelpers.ts
│ 📄 userHelpers.test.ts
📄 types.ts
📄 constants.ts
📄 index.ts // exports publics
📄 users.test.ts // tests d'intégration
Tester la feature complète
// features/users/users.test.ts
import { render, screen } from '@testing-library/react'
import { test, expect } from 'vitest'
import { UserList } from './components/UserList'
import { UserProvider } from './hooks/useUsers'
test('affiche la liste complète des utilisateurs', async () => {
render(
<UserProvider>
<UserList />
</UserProvider>
)
expect(screen.getByText('Utilisateurs')).toBeInTheDocument()
})
💡 Teste l'interaction entre tous les composants de la feature
Adapter la configuration pour trouver tous les tests
// vitest.config.ts
export default defineConfig({
test: {
environment: 'jsdom',
globals: true,
setupFiles: './src/setupTests.ts',
include: [
'**/*.{test,spec}.{js,ts,jsx,tsx}'
],
exclude: [
'node_modules',
'dist'
],
}
})
✅ Vitest trouvera automatiquement tous les .test.ts dans features/
Lancer les tests spécifiques à une feature
// package.json
{
"scripts": {
"test": "vitest",
"test:ui": "vitest --ui",
"test:users": "vitest features/users",
"test:products": "vitest features/products",
"test:watch": "vitest --watch",
"test:coverage": "vitest --coverage",
}
}
🚀 npm run test:users ne teste que la feature users!
🏗️ Structure
🧪 Tests
📦 Maintenance
⚡ Performance
Les erreurs à éviter
✗ MAUVAIS
expect(component.state.count)
.toBe(5)
Teste les détails internes
✓ CORRECT
expect(screen.getByText('5'))
.toBeInTheDocument()
Teste le RÉSULTAT visible
✗ É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
✗ ERREUR
user.click(button)
// Pas de await → test flaky
userEvent est ASYNC !
✓ CORRECT
await user.click(button)
Toujours await userEvent
✗ ERREUR
expect(
screen.getByText('Erreur')
).not.toBeInTheDocument()
// Throw si pas trouvé!
✓ CORRECT
expect(
screen.queryByText('Erreur')
).not.toBeInTheDocument()
// Retourne null si absent
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
Feature Tests
Co-localisez tests avec le code dans features/
Organisation
1 test file par composant + tests d'intégration
Vous savez maintenant tester vos composants React
Questions ?