Allez plus loin dans vos tests : simulez les appels API

Testez des composants qui font des appels API

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.

Installez MSW (Mock Service Worker)

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éez un composant qui fait un appel API

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

Configurez MSW pour vos tests

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

Testez le composant avec des appels API

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.

Testez différents scénarios de réponse API

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

À vous de jouer !

Dans la branche P1C4-Begin vous trouverez un composant  PostsList  qui :

  1. Affiche une liste d'articles récupérés depuis/api/posts

  2. Affiche un loader pendant le chargement

  3. Affiche un message d'erreur en cas d'échec

  4. Permet de supprimer un article (DELETE/api/posts/:id)

Écrivez les tests pour tous ces scénarios avec MSW.

En résumé

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

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