
Jusqu'à présent, nous avons testé des composants avec de la logique interne.
Mais que se passe-t-il quand un composant fait un appel API ?
Le problème : Nous ne voulons pas faire de vrais appels API dans nos tests car :
Ils sont lents
Ils dépendent d'un serveur externe
Ils peuvent échouer pour des raisons qui n'ont rien à voir avec notre code
Ils peuvent modifier des données en base
La solution : Simuler les appels API avec des mocks.
MSW intercepte les requêtes réseau et retourne des données simulées. C'est l'outil recommandé par React Testing Library.
npm install -D msw@latest
Créons un composant classique pour une application, un profil utilisateursrc/components/UserProfile/UserProfile.jsx :
import { useState, useEffect } from 'react'
export function UserProfile({ userId }) {
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
async function fetchUser() {
try {
setLoading(true)
setError(null)
const response = await fetch(`/api/users/${userId}`)
if (!response.ok) {
throw new Error('Utilisateur non trouvé')
}
const data = await response.json()
setUser(data)
} catch (err) {
setError(err.message)
} finally {
setLoading(false)
}
}
fetchUser()
}, [userId])
if (loading) {
return <div data-testid="loading">Chargement...</div>
}
if (error) {
return <div role="alert">Erreur : {error}</div>
}
return (
<div>
<h2>{user.name}</h2>
<p>Email : {user.email}</p>
<p>Ville : {user.city}</p>
</div>
)
}Avant de rédiger nos tests il est important de mettre en place quelques éléments additionnels : les handlers. Cette fonctionnalité nous permettent de définir les requêtes à intercepter et quelles valeurs retourner lorsqu'un appel API est effectué.
Voyons comment cela fonctionne par l'exemple
Créez src/test/mocks/handlers.js :
import { http, HttpResponse } from 'msw'
export const handlers = [
// Simuler GET /api/users/:id
http.get( // 1 - Définition de la méthode HTTP
'/api/users/:id', // 2 - Définition de la route
({ params }) => { // 3 - Définition de la fonction de traitement
const { id } = params // 4 - Récupération des paramètres de la route
// Simuler une réponse réussie
return HttpResponse.json({
id: parseInt(id),
name: 'John Doe',
email: 'john@example.com',
city: 'Paris',
})
}),
]Afin de pouvoir servir nos requêtes il va falloir que l'on créé un serveur msw
Créez src/test/mocks/server.js :
import { setupServer } from 'msw/node'
import { handlers } from './handlers'
export const server = setupServer(...handlers)Mettez à jour src/test/setup.js :
import { expect, afterEach, beforeAll, afterAll } from 'vitest'
import { cleanup } from '@testing-library/react'
import * as matchers from '@testing-library/jest-dom/matchers'
import { server } from './mocks/server'
expect.extend(matchers)
// Démarre le serveur MSW avant tous les tests
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
// Nettoie après chaque test
afterEach(() => {
cleanup()
server.resetHandlers()
})
// Arrête le serveur après tous les tests
afterAll(() => server.close())Créez src/components/UserProfile/UserProfile.test.jsx :
import { describe, it, expect } from 'vitest'
import { render, screen, waitFor } from '@testing-library/react'
import { http, HttpResponse } from 'msw'
import { server } from '../../test/mocks/server'
import { UserProfile } from './UserProfile'
describe('UserProfile', () => {
it('should show loading state initially', () => {
render(<UserProfile userId={1} />)
expect(screen.getByTestId('loading')).toBeInTheDocument()
})
it('should display user data after successful fetch', async () => {
render(<UserProfile userId={1} />)
// Attendre que le chargement soit terminé
await waitFor(() => {
expect(screen.queryByTestId('loading')).not.toBeInTheDocument()
})
// Vérifier que les données sont affichées
expect(screen.getByText('John Doe')).toBeInTheDocument()
expect(screen.getByText(/john@example.com/)).toBeInTheDocument()
expect(screen.getByText(/Paris/)).toBeInTheDocument()
})
it('should display error message when fetch fails', async () => {
// Surcharger le handler pour ce test spécifique
server.use(
http.get('/api/users/:id', () => {
return new HttpResponse(null, { status: 404 })
})
)
render(<UserProfile userId={999} />)
// Attendre que l'erreur s'affiche
await waitFor(() => {
expect(screen.getByRole('alert')).toHaveTextContent('Utilisateur non trouvé')
})
})
it('should fetch new user when userId changes', async () => {
const { rerender } = render(<UserProfile userId={1} />)
// Attendre le premier chargement
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument()
})
// Changer de mock pour le prochain appel
server.use(
http.get('/api/users/:id', ({ params }) => {
return HttpResponse.json({
id: parseInt(params.id),
name: 'Jane Smith',
email: 'jane@example.com',
city: 'Lyon',
})
})
)
// Rerender avec un nouveau userId
rerender(<UserProfile userId={2} />)
// Vérifier que les nouvelles données s'affichent
await waitFor(() => {
expect(screen.getByText('Jane Smith')).toBeInTheDocument()
expect(screen.getByText(/jane@example.com/)).toBeInTheDocument()
})
})
})Après cet exemple je vous dois quelques explication, en effet vous avez certainement pu voir dans cet exemple quelques concepts que nous n'avons pas encore mentionné et qui sont pourtant très utils au moment de réaliser nos tests :
lignes 30 à 33 : nous avons modifié ce que devrait renvoyer l'API mockée pour un test spécifique, c'est pratique lorsque l'on veut ponctuellement tester un comportement particulier.
ligne 45 le rerender : cette fonction nous permet ici de tester plusieurs fois notre composant. Dans notre cas une première fois avec l'userId 1 puis ensuite avec l'userId 2. Qu'avons nous fait entre les deux appels ? Comme le montre les lignes 54 à 62 nous avons modifié le mock de la fonction qui récupère les utilisateurs et nous vérifions donc ainsi que lorsque le composant est rendu a nouveau avec les nouvelles données alors les bonnes informations sont affichées.
Jusqu'ici nous avons testé les cas les plus courants de réponses d' API. Voici a continuation quelques exemples d'autres cas que vous pouvez prendre en compte.
// Succès avec délai
server.use(
http.get('/api/users/:id', async () => {
await delay(100) // Attendre 100ms
return HttpResponse.json({ name: 'John' })
})
)
// Erreur réseau
server.use(
http.get('/api/users/:id', () => {
return HttpResponse.error()
})
)
// Réponse vide
server.use(
http.get('/api/users/:id', () => {
return new HttpResponse(null, { status: 204 })
})
)
// Réponse avec headers personnalisés
server.use(
http.get('/api/users/:id', () => {
return HttpResponse.json(
{ name: 'John' },
{ headers: { 'X-Custom-Header': 'value' } }
)
})
)
Dans la branche P1C4-Begin vous trouverez un composant PostsList qui :
Affiche une liste d'articles récupérés depuis/api/posts
Affiche un loader pendant le chargement
Affiche un message d'erreur en cas d'échec
Permet de supprimer un article (DELETE/api/posts/:id)
Écrivez les tests pour tous ces scénarios avec MSW.
MSW intercepte les requêtes HTTP et retourne des données simulées
Configurez MSW danssetup.jspour tous vos tests
Utilisezserver.use()pour surcharger les handlers dans des tests spécifiques
waitFor()permet d'attendre les opérations asynchrones
Félicitations ! Vous maîtrisez maintenant les tests en React. Il est temps de valider vos connaissances avec le quiz final !