Tests React en 2026 : Vitest, React Testing Library et Bonnes Pratiques
Maîtriser les tests React avec Vitest et React Testing Library. Découvrir les patterns de test de composants, la gestion asynchrone, les stratégies de mocking et les bonnes pratiques pour les entretiens techniques en 2026.

Les tests React en 2026 reposent sur Vitest comme exécuteur de tests dominant et React Testing Library (RTL) comme standard pour les tests de composants. Cette combinaison offre des boucles de feedback rapides, un support natif des modules ES et des tests qui vérifient le comportement plutôt que les détails d'implémentation.
Écrire des tests qui ressemblent à la façon dont les utilisateurs interagissent avec les composants. Interroger par rôles accessibles, labels et texte—pas par classes CSS ou attributs data-testid. Cette approche détecte les vrais bugs et survit au refactoring.
Configuration de Vitest pour les Projets React
Vitest a remplacé Jest comme exécuteur de tests préféré pour les applications React modernes. Le support natif des modules ES élimine le surcoût de transformation, et l'intégration étroite avec Vite signifie que la configuration reflète l'environnement de développement.
Une configuration Vitest prête pour la production gère la transformation JSX, les alias de chemins et la configuration de l'environnement de test sans boilerplate :
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import tsconfigPaths from 'vite-tsconfig-paths'
export default defineConfig({
plugins: [react(), tsconfigPaths()],
test: {
// Use jsdom for DOM APIs and React rendering
environment: 'jsdom',
// Run setup file before each test file
setupFiles: ['./src/test/setup.ts'],
// Include only test files, exclude e2e
include: ['src/**/*.{test,spec}.{ts,tsx}'],
// Enable global test APIs (describe, it, expect)
globals: true,
// Generate coverage reports
coverage: {
provider: 'v8',
reporter: ['text', 'html', 'lcov'],
exclude: ['node_modules', 'src/test/**'],
},
},
})Le fichier de setup étend les matchers et configure le nettoyage entre les tests :
import '@testing-library/jest-dom/vitest'
import { cleanup } from '@testing-library/react'
import { afterEach, vi } from 'vitest'
// Clean up rendered components after each test
afterEach(() => {
cleanup()
})
// Mock window.matchMedia for responsive components
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation((query: string) => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
})Cette configuration fournit une base propre pour chaque test et gère les mocks courants des API navigateur que jsdom ne possède pas.
Tests de Composants avec React Testing Library
React Testing Library impose de tester les composants du point de vue de l'utilisateur. Au lieu d'inspecter l'état interne ou les valeurs des props, les tests interagissent avec le rendu via des requêtes d'accessibilité.
Considérons un composant de recherche qui filtre les résultats et affiche les états de chargement :
import { useState } from 'react'
interface SearchResultsProps {
onSearch: (query: string) => Promise<string[]>
}
export function SearchResults({ onSearch }: SearchResultsProps) {
const [query, setQuery] = useState('')
const [results, setResults] = useState<string[]>([])
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
async function handleSearch() {
if (!query.trim()) return
setIsLoading(true)
setError(null)
try {
const data = await onSearch(query)
setResults(data)
} catch (e) {
setError('Search failed. Please try again.')
} finally {
setIsLoading(false)
}
}
return (
<div>
<label htmlFor="search-input">Search</label>
<input
id="search-input"
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<button onClick={handleSearch} disabled={isLoading}>
{isLoading ? 'Searching...' : 'Search'}
</button>
{error && <p role="alert">{error}</p>}
<ul aria-label="Search results">
{results.map((result, i) => (
<li key={i}>{result}</li>
))}
</ul>
</div>
)
}Les tests vérifient le comportement du composant à travers les interactions utilisateur et les états asynchrones :
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, it, expect, vi } from 'vitest'
import { SearchResults } from './SearchResults'
describe('SearchResults', () => {
it('displays results after successful search', async () => {
// Arrange: mock the search function
const mockSearch = vi.fn().mockResolvedValue(['React', 'Vue', 'Angular'])
const user = userEvent.setup()
render(<SearchResults onSearch={mockSearch} />)
// Act: type a query and click search
await user.type(screen.getByLabelText('Search'), 'framework')
await user.click(screen.getByRole('button', { name: 'Search' }))
// Assert: results appear in the list
expect(await screen.findByText('React')).toBeInTheDocument()
expect(screen.getByText('Vue')).toBeInTheDocument()
expect(mockSearch).toHaveBeenCalledWith('framework')
})
it('shows loading state during search', async () => {
// Create a promise that doesn't resolve immediately
let resolveSearch: (value: string[]) => void
const searchPromise = new Promise<string[]>((resolve) => {
resolveSearch = resolve
})
const mockSearch = vi.fn().mockReturnValue(searchPromise)
const user = userEvent.setup()
render(<SearchResults onSearch={mockSearch} />)
await user.type(screen.getByLabelText('Search'), 'test')
await user.click(screen.getByRole('button', { name: 'Search' }))
// Button shows loading state
expect(screen.getByRole('button', { name: 'Searching...' })).toBeDisabled()
// Resolve the search to clean up
resolveSearch!([])
})
it('displays error message on search failure', async () => {
const mockSearch = vi.fn().mockRejectedValue(new Error('Network error'))
const user = userEvent.setup()
render(<SearchResults onSearch={mockSearch} />)
await user.type(screen.getByLabelText('Search'), 'query')
await user.click(screen.getByRole('button', { name: 'Search' }))
// Error message appears with alert role for screen readers
expect(await screen.findByRole('alert')).toHaveTextContent(
'Search failed. Please try again.'
)
})
})La bibliothèque userEvent simule des interactions utilisateur réalistes incluant les événements clavier et la gestion du focus, détectant des bugs que les événements synthétiques manquent.
Patterns de Tests Asynchrones et waitFor
Les opérations asynchrones nécessitent des stratégies d'attente explicites. React Testing Library fournit les requêtes findBy qui combinent getBy avec une attente automatique, plus waitFor pour les assertions complexes.
Préférer findBy à waitFor + getBy pour attendre l'apparition d'éléments. Réserver waitFor pour les assertions sur des éléments existants qui changent d'état.
Les patterns asynchrones courants apparaissent lors des tests de hooks et composants de récupération de données :
import { render, screen, waitFor, waitForElementToBeRemoved } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, it, expect, vi } from 'vitest'
// Wait for element to appear (preferred for new elements)
it('loads user profile on mount', async () => {
render(<UserProfile userId="123" />)
// findBy returns a promise, automatically waits up to 1000ms
const heading = await screen.findByRole('heading', { name: /john doe/i })
expect(heading).toBeInTheDocument()
})
// Wait for element to disappear
it('removes loading spinner after data loads', async () => {
render(<DataTable />)
// Wait for spinner to be removed from DOM
await waitForElementToBeRemoved(() => screen.queryByRole('progressbar'))
expect(screen.getByRole('table')).toBeInTheDocument()
})
// Wait for multiple conditions
it('enables submit button when form is valid', async () => {
const user = userEvent.setup()
render(<RegistrationForm />)
const submitButton = screen.getByRole('button', { name: 'Submit' })
expect(submitButton).toBeDisabled()
await user.type(screen.getByLabelText('Email'), 'user@example.com')
await user.type(screen.getByLabelText('Password'), 'SecurePass123!')
// waitFor retries the assertion until it passes or times out
await waitFor(() => {
expect(submitButton).toBeEnabled()
})
})
// Avoid false positives with queryBy for absence checks
it('does not show premium badge for free users', async () => {
render(<UserCard subscription="free" />)
// queryBy returns null instead of throwing, use for absence checks
expect(screen.queryByText('Premium')).not.toBeInTheDocument()
})La personnalisation des timeouts aide avec les opérations lentes sans ralentir l'ensemble de la suite :
// Increase timeout for slow operations
await screen.findByText('Upload complete', {}, { timeout: 5000 })
await waitFor(
() => expect(mockSubmit).toHaveBeenCalled(),
{ timeout: 3000, interval: 100 }
)Prêt à réussir tes entretiens React / Next.js ?
Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.
Stratégies de Mocking : Modules, APIs et Hooks
Les capacités de mocking de Vitest gèrent les dépendances externes sans polluer le graphe de modules. Le mocking stratégique isole les composants des appels réseau, bibliothèques tierces et dépendances complexes.
Le mocking de modules remplace les imports au niveau du bundler :
import { vi, describe, it, expect, beforeEach } from 'vitest'
import { fetchUserData } from './api'
// Mock the entire fetch module
vi.mock('./http-client', () => ({
httpClient: {
get: vi.fn(),
},
}))
import { httpClient } from './http-client'
describe('fetchUserData', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('transforms API response into user model', async () => {
// Type-safe mock implementation
vi.mocked(httpClient.get).mockResolvedValue({
data: { id: 1, first_name: 'John', last_name: 'Doe' },
})
const user = await fetchUserData(1)
expect(user).toEqual({
id: 1,
fullName: 'John Doe',
})
})
})Pour les hooks React qui dépendent du contexte ou de l'état externe, l'utilitaire renderHook permet des tests isolés :
import { renderHook, waitFor } from '@testing-library/react'
import { describe, it, expect, vi } from 'vitest'
import { useAuth } from './useAuth'
import { AuthProvider } from './AuthContext'
describe('useAuth', () => {
it('returns authenticated user after login', async () => {
const mockLogin = vi.fn().mockResolvedValue({ id: 1, name: 'Alice' })
const { result } = renderHook(() => useAuth(), {
wrapper: ({ children }) => (
<AuthProvider loginFn={mockLogin}>{children}</AuthProvider>
),
})
expect(result.current.user).toBeNull()
expect(result.current.isAuthenticated).toBe(false)
await result.current.login('alice@example.com', 'password')
await waitFor(() => {
expect(result.current.user).toEqual({ id: 1, name: 'Alice' })
expect(result.current.isAuthenticated).toBe(true)
})
})
})MSW (Mock Service Worker) fournit le mocking d'API au niveau réseau, permettant des tests qui exercent les appels fetch réels :
import { http, HttpResponse } from 'msw'
export const handlers = [
http.get('/api/users/:id', ({ params }) => {
return HttpResponse.json({
id: params.id,
name: 'Test User',
})
}),
http.post('/api/login', async ({ request }) => {
const body = await request.json()
if (body.email === 'valid@example.com') {
return HttpResponse.json({ token: 'abc123' })
}
return HttpResponse.json(
{ error: 'Invalid credentials' },
{ status: 401 }
)
}),
]Tester les Server Components de React 19
Les Server Components présentent des défis de test uniques puisqu'ils s'exécutent sur le serveur et streament le HTML vers le client. Les tests unitaires directs nécessitent une approche différente des tests de composants client.
Pour les Server Components qui effectuent de la récupération de données, tester le rendu avec des sources de données mockées :
import { describe, it, expect, vi } from 'vitest'
// Mock the data fetching function
vi.mock('./db', () => ({
getUser: vi.fn().mockResolvedValue({ id: 1, name: 'Server User' }),
}))
import { render, screen } from '@testing-library/react'
import { UserProfile } from './UserProfile.server'
describe('UserProfile Server Component', () => {
it('renders user data from database', async () => {
// Server components are async, await the component
const Component = await UserProfile({ userId: '1' })
render(Component)
expect(screen.getByRole('heading')).toHaveTextContent('Server User')
})
})Les tests d'intégration avec Playwright fournissent la couverture la plus fiable pour les Server Components en testant le pipeline de rendu complet :
import { test, expect } from '@playwright/test'
test('server component renders with streamed data', async ({ page }) => {
await page.goto('/users/1')
// Wait for streaming to complete
await expect(page.getByRole('heading', { name: /profile/i })).toBeVisible()
await expect(page.getByText('Server User')).toBeVisible()
})Tester les Custom Hooks avec État Complexe
Les hooks personnalisés qui gèrent un état complexe, des effets de bord ou des abonnements externes bénéficient de tests dédiés. L'utilitaire renderHook de React Testing Library gère le cycle de vie et les mises à jour des hooks.
Considérons un hook de recherche avec debounce qui se coordonne avec une API :
import { useState, useEffect, useCallback } from 'react'
export function useDebounceSearch<T>(
searchFn: (query: string) => Promise<T[]>,
delay = 300
) {
const [query, setQuery] = useState('')
const [results, setResults] = useState<T[]>([])
const [isLoading, setIsLoading] = useState(false)
useEffect(() => {
if (!query) {
setResults([])
return
}
setIsLoading(true)
const timeoutId = setTimeout(async () => {
try {
const data = await searchFn(query)
setResults(data)
} finally {
setIsLoading(false)
}
}, delay)
return () => clearTimeout(timeoutId)
}, [query, searchFn, delay])
return { query, setQuery, results, isLoading }
}Les tests vérifient le comportement du debouncing et les transitions d'état :
import { renderHook, act, waitFor } from '@testing-library/react'
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { useDebounceSearch } from './useDebounceSearch'
describe('useDebounceSearch', () => {
beforeEach(() => {
vi.useFakeTimers()
})
it('debounces search calls', async () => {
const mockSearch = vi.fn().mockResolvedValue(['result1', 'result2'])
const { result } = renderHook(() => useDebounceSearch(mockSearch, 300))
// Type multiple characters quickly
act(() => result.current.setQuery('r'))
act(() => result.current.setQuery('re'))
act(() => result.current.setQuery('rea'))
act(() => result.current.setQuery('reac'))
act(() => result.current.setQuery('react'))
// Search not called yet (debouncing)
expect(mockSearch).not.toHaveBeenCalled()
// Advance timers past debounce delay
await act(async () => {
vi.advanceTimersByTime(300)
})
// Only one search call with final query
expect(mockSearch).toHaveBeenCalledTimes(1)
expect(mockSearch).toHaveBeenCalledWith('react')
})
it('shows loading state during search', async () => {
let resolveSearch: (value: string[]) => void
const mockSearch = vi.fn().mockImplementation(
() => new Promise((resolve) => { resolveSearch = resolve })
)
const { result } = renderHook(() => useDebounceSearch(mockSearch, 100))
act(() => result.current.setQuery('test'))
await act(async () => {
vi.advanceTimersByTime(100)
})
expect(result.current.isLoading).toBe(true)
await act(async () => {
resolveSearch(['result'])
})
expect(result.current.isLoading).toBe(false)
expect(result.current.results).toEqual(['result'])
})
})Les fake timers de Vitest permettent des tests déterministes du comportement dépendant du temps sans délais réels.
Questions d'Entretien sur les Tests React
Les entretiens techniques sondent fréquemment les connaissances en testing. Ces questions évaluent la compréhension pratique plutôt que les concepts théoriques.
Éviter de tester les détails d'implémentation comme les valeurs d'état ou les méthodes de composants. Les tests qui cassent lors du refactoring de la structure interne fournissent une fausse confiance et un fardeau de maintenance.
Priorité des requêtes dans React Testing Library
La priorité de requête recommandée suit l'accessibilité : getByRole > getByLabelText > getByPlaceholderText > getByText > getByTestId. Les requêtes basées sur les rôles garantissent que les composants restent accessibles et que les tests survivent aux changements de markup.
Quand utiliser findBy vs waitFor
Les requêtes findBy attendent que les éléments apparaissent dans le DOM et retournent une promise. Les utiliser lorsqu'on attend de nouveaux éléments après des opérations asynchrones. waitFor enveloppe les assertions qui peuvent ne pas passer immédiatement, réessayant jusqu'au succès ou timeout. Utiliser waitFor pour asserter sur les changements d'état des éléments existants.
Mocking fetch vs MSW
Le mocking direct de fetch (vi.spyOn(global, 'fetch')) fonctionne pour les cas simples mais nécessite de réimplémenter les objets Response et la gestion d'erreurs. MSW intercepte au niveau réseau, permettant aux tests d'exercer les appels fetch réels, la sérialisation des requêtes et les chemins de code de gestion d'erreurs.
Pour une pratique approfondie de ces concepts, explorer les questions d'entretien React Testing sur SharpSkill.
Conclusion
- Configurer Vitest avec l'environnement jsdom, les fichiers de setup pour les extensions de matchers et les rapports de couverture pour la visibilité sur les lacunes de couverture
- Interroger les composants par rôles et labels accessibles pour écrire des tests qui vérifient le comportement face à l'utilisateur et détectent les vrais bugs
- Utiliser les requêtes
findBypour les éléments qui apparaissent après des opérations asynchrones,waitForpour les assertions sur l'état changeant - Mocker au niveau approprié : mocks de modules pour l'isolation unitaire, MSW pour des tests réalistes de la couche réseau
- Tester les hooks personnalisés avec
renderHooket fake timers pour vérifier la gestion d'état complexe et les effets de bord - Les Server Components nécessitent soit un rendu asynchrone dans les tests soit des tests d'intégration complets avec Playwright
- Lectures connexes : Patterns avancés de React Hooks et TypeScript avec React pour les patterns de test type-safe
Passe à la pratique !
Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.
Tags
Partager
Articles similaires

React Compiler en 2026 : mémoïsation automatique et questions d'entretien
Analyse approfondie du React Compiler en 2026 : pipeline de compilation, mémoïsation automatique, règles de React et questions posées en entretien technique.

Top 30 questions d'entretien React : guide complet pour réussir
Les 30 questions d'entretien React les plus posées en 2026. Réponses détaillées, exemples de code et conseils pour décrocher votre poste de développeur React.

Next.js 16 Server Actions en 2026 : mutations, revalidation et questions d'entretien
Comment les Server Actions de Next.js 16 gèrent les mutations, la revalidation, l'état pending, l'UI optimiste et la sécurité, avec les questions d'entretien qui testent chaque concept.