Changer le thème

Tests unitaires Next.js : guide complet Jest + React Testing Library

Easton editorial illustration: component assembly loom

Lundi, 10 h : le tech lead poste dans le groupe : « Cette semaine, on ajoute des tests unitaires — Next.js avec Jest. »

J’avais déjà testé du React, mais App Router et Server Components, page blanche. npm install jest, npm test, et tout passe au rouge :

Error: Cannot use import statement outside a module
SyntaxError: Unexpected token 'export'
Cannot find module 'next/navigation'

Deux jours entre fichiers de config, Stack Overflow et GitHub Issues. Quand les tests ont enfin passé, j’ai failli jeter le clavier en l’air.

La config de test Next.js est plus pénible qu’on ne le croit, mais pas sorcière : quelques réglages clés et les pièges habituels suffisent. Ce guide configure Next.js 15 + Jest + React Testing Library et couvre Client/Server Components, hooks et mocks d’API.

Configuration de l’environnement de test (de zéro à opérationnel)

Commençons par faire tourner l’environnement. Section un peu sèche, mais chaque option est expliquée pour ne pas copier-coller sans comprendre.

Installation des dépendances

Dans le terminal, installez tout d’un coup :

npm install -D jest jest-environment-jsdom @testing-library/react @testing-library/dom @testing-library/jest-dom ts-node @types/jest

Rôle de chaque paquet :

  • jest : le framework de test
  • jest-environment-jsdom : simule le navigateur (DOM requis pour React)
  • @testing-library/react : utilitaires de test de composants React
  • @testing-library/jest-dom : assertions étendues (ex. toBeInTheDocument())
  • ts-node et @types/jest : support TypeScript (optionnel en JS)

Après l’installation, ne lancez pas les tests tout de suite : les fichiers de config manquent encore.

Créer jest.config.ts

Fichier le plus important. Créez jest.config.ts à la racine (.js si vous êtes en JavaScript) :

import type { Config } from 'jest'
import nextJest from 'next/jest'

// Charge automatiquement la configuration Next.js
const createJestConfig = nextJest({
  dir: './', // racine du projet Next.js
})

const config: Config = {
  coverageProvider: 'v8', // outil de couverture de code
  testEnvironment: 'jsdom', // environnement navigateur simulé
  setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'], // exécuté avant les tests
}

// createJestConfig applique les transformations Next.js
export default createJestConfig(config)

Point clé : pourquoi envelopper avec next/jest ?

next/jest gère automatiquement :

  • les fichiers .css / .module.css (mock automatique, sinon erreur en test)
  • images, polices et assets statiques
  • le chargement des variables .env
  • la transformation TypeScript et JSX
  • l’exclusion de node_modules et .next

Sans ça, tout est à configurer à la main. Croyez-moi, c’est pénible.

Créer jest.setup.ts

À la racine, créez jest.setup.ts, contenu minimal :

import '@testing-library/jest-dom'

Cette ligne importe les matchers Jest DOM, par exemple :

  • expect(element).toBeInTheDocument()
  • expect(element).toHaveClass('active')
  • expect(element).toBeVisible()

Sans ce fichier, ces méthodes ne sont pas reconnues.

Alias de chemins (si vous utilisez @/)

Si vous importez avec @/components/Button, indiquez à Jest comment résoudre ces chemins.

Vérifiez que tsconfig.json (ou jsconfig.json) contient :

{
  "compilerOptions": {
    "baseUrl": "./",
    "paths": {
      "@/components/*": ["components/*"],
      "@/lib/*": ["lib/*"]
    }
  }
}

Ajoutez moduleNameMapper dans jest.config.ts :

const config: Config = {
  coverageProvider: 'v8',
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
  // alias de chemins
  moduleNameMapper: {
    '^@/components/(.*)$': '<rootDir>/components/$1',
    '^@/lib/(.*)$': '<rootDir>/lib/$1',
  },
}

Pourquoi ? Jest ne connaît pas @/ par défaut. Il faut mapper @/components/Button vers <rootDir>/components/Button.

Scripts de test

Ajoutez dans package.json :

{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch"
  }
}
  • npm test : exécuter tous les tests une fois
  • npm run test:watch : mode watch, relance auto à chaque modification

Vérifier la configuration

Créez __tests__/example.test.ts pour valider :

describe('Example Test', () => {
  it('should pass', () => {
    expect(1 + 1).toBe(2)
  })
})

Lancez npm test. Un PASS vert et 1 passed : la config est bonne.

En cas d’erreur, voyez le chapitre 5 — 90 % des cas y sont couverts.

Tests de composants en pratique

Config OK : passons aux vrais tests. Cœur du sujet Next.js, mais Client et Server Components ne se testent pas pareil.

Tester les Client Components (flux standard)

Les Client Components ('use client') se testent de façon classique.

Exemple : formulaire de connexion LoginForm.tsx :

'use client'

import { useState } from 'react'

export default function LoginForm() {
  const [email, setEmail] = useState('')
  const [error, setError] = useState('')

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault()
    if (!email.includes('@')) {
      setError('Veuillez saisir un e-mail valide')
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="E-mail"
      />
      {error && <span role="alert">{error}</span>}
      <button type="submit">Connexion</button>
    </form>
  )
}

Test file LoginForm.test.tsx:

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

describe('LoginForm', () => {
  it('should render input and button', () => {
    render(<LoginForm />)

    // Vérifier la présence du champ
    const emailInput = screen.getByPlaceholderText('E-mail')
    expect(emailInput).toBeInTheDocument()

    // Vérifier la présence du bouton
    const submitButton = screen.getByRole('button', { name: 'Connexion' })
    expect(submitButton).toBeInTheDocument()
  })

  it('should show error for invalid email', () => {
    render(<LoginForm />)

    const emailInput = screen.getByPlaceholderText('E-mail')
    const submitButton = screen.getByRole('button', { name: 'Connexion' })

    // Simuler la saisie
    fireEvent.change(emailInput, { target: { value: 'invalid-email' } })
    fireEvent.click(submitButton)

    // Vérifier le message d'erreur
    const errorMessage = screen.getByRole('alert')
    expect(errorMessage).toHaveTextContent('Veuillez saisir un e-mail valide')
  })
})

Méthode :

  1. render() pour monter le composant
  2. screen.getByXxx() pour cibler les éléments (rôle, texte, placeholder…)
  3. fireEvent pour simuler l’utilisateur
  4. expect() pour les assertions

Astuce : préférez getByRole à getByTestId — plus proche de ce que voit l’utilisateur, tests plus stables.

Server Components (un peu délicat)

Les Server Components sont centraux en Next.js 15, mais Jest les gère mal.

Problème : Jest ne supporte pas les Server Components async.

Exemple : composant qui charge des données :

// app/posts/page.tsx (Server Component)
async function getPosts() {
  const res = await fetch('https://api.example.com/posts')
  return res.json()
}

export default async function PostsPage() {
  const posts = await getPosts()

  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

Test direct : erreur Objects are not valid as a React child.

Que faire ?

Trois approches :

Approche 1 : extraire la logique, tester des fonctions pures

// lib/posts.ts
export async function getPosts() {
  const res = await fetch('https://api.example.com/posts')
  if (!res.ok) throw new Error('Failed to fetch')
  return res.json()
}

// lib/posts.test.ts
import { getPosts } from './posts'

describe('getPosts', () => {
  it('should fetch posts successfully', async () => {
    global.fetch = jest.fn(() =>
      Promise.resolve({
        ok: true,
        json: () => Promise.resolve([{ id: 1, title: 'Test Post' }]),
      })
    ) as jest.Mock

    const posts = await getPosts()
    expect(posts).toHaveLength(1)
    expect(posts[0].title).toBe('Test Post')
  })
})

On teste la récupération de données, pas le rendu du composant. Logique async à part, composant simple.

Approche 2 : Server Components synchrones

Sans async, le test classique fonctionne :

// components/Title.tsx (Server Component, no async)
export default function Title({ text }: { text: string }) {
  return <h1 className="title">{text}</h1>
}

// components/Title.test.tsx
import { render, screen } from '@testing-library/react'
import Title from './Title'

describe('Title', () => {
  it('should render title', () => {
    render(<Title text="Hello World" />)
    const heading = screen.getByRole('heading', { level: 1 })
    expect(heading).toHaveTextContent('Hello World')
  })
})

Approche 3 : compléter avec l’E2E

Pour les Server Components complexes, Playwright ou Cypress en E2E est plus fiable. Jest pour la logique, E2E pour le parcours complet.

Ma pratique : logique métier en fonctions pures testées à part ; Server Components simples ; couverture complète en E2E.

Astuces pour les interactions

Pour les interactions, @testing-library/react offre :

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

// Clic
fireEvent.click(button)

// Saisie
fireEvent.change(input, { target: { value: 'test' } })

// Attendre une mise à jour async
await waitFor(() => {
  expect(screen.getByText('Success')).toBeInTheDocument()
})

// Visibilité
expect(element).toBeVisible()

// Classe CSS
expect(element).toHaveClass('active')

Exemple de test async complet :

it('should submit form successfully', async () => {
  // Mock de l'API
  global.fetch = jest.fn(() =>
    Promise.resolve({
      ok: true,
      json: () => Promise.resolve({ success: true }),
    })
  ) as jest.Mock

  render(<LoginForm />)

  const emailInput = screen.getByPlaceholderText('E-mail')
  const submitButton = screen.getByRole('button', { name: 'Connexion' })

  fireEvent.change(emailInput, { target: { value: '[email protected]' } })
  fireEvent.click(submitButton)

  // Wait for success message to appear
  await waitFor(() => {
    expect(screen.getByText('Connexion réussie')).toBeInTheDocument()
  })
})

waitFor attend la fin des opérations async. Avec useEffect ou état async, indispensable — sinon assertion trop tôt et faux négatifs.

Stratégies de test des hooks

Comment tester un hook custom sans mélanger logique hook et composant ? renderHook.

Hooks simples

Exemple : hook compteur :

// hooks/useCounter.ts
import { useState } from 'react'

export function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue)

  const increment = () => setCount(c => c + 1)
  const decrement = () => setCount(c => c - 1)
  const reset = () => setCount(initialValue)

  return { count, increment, decrement, reset }
}

Test :

// hooks/useCounter.test.ts
import { renderHook, act } from '@testing-library/react'
import { useCounter } from './useCounter'

describe('useCounter', () => {
  it('should initialize with default value', () => {
    const { result } = renderHook(() => useCounter())
    expect(result.current.count).toBe(0)
  })

  it('should initialize with custom value', () => {
    const { result } = renderHook(() => useCounter(10))
    expect(result.current.count).toBe(10)
  })

  it('should increment count', () => {
    const { result } = renderHook(() => useCounter())

    act(() => {
      result.current.increment()
    })

    expect(result.current.count).toBe(1)
  })

  it('should reset count', () => {
    const { result } = renderHook(() => useCounter(5))

    act(() => {
      result.current.increment()
      result.current.increment()
    })

    expect(result.current.count).toBe(7)

    act(() => {
      result.current.reset()
    })

    expect(result.current.count).toBe(5)
  })
})

Points clés :

  • renderHook monte le hook
  • act enveloppe les mises à jour d’état (règle React)
  • result.current lit la valeur retournée

Hooks dépendants d’un Context

Avec un Context (ex. auth), fournissez un Provider.

// hooks/useAuth.ts
import { useContext } from 'react'
import { AuthContext } from '@/contexts/AuthContext'

export function useAuth() {
  const context = useContext(AuthContext)
  if (!context) {
    throw new Error('useAuth must be used within AuthProvider')
  }
  return context
}

Test avec Provider mocké :

// hooks/useAuth.test.tsx
import { renderHook } from '@testing-library/react'
import { useAuth } from './useAuth'
import { AuthContext } from '@/contexts/AuthContext'

describe('useAuth', () => {
  it('should return auth context value', () => {
    const mockAuthValue = {
      user: { id: 1, name: 'Test User' },
      login: jest.fn(),
      logout: jest.fn(),
    }

    const wrapper = ({ children }: { children: React.ReactNode }) => (
      <AuthContext.Provider value={mockAuthValue}>
        {children}
      </AuthContext.Provider>
    )

    const { result } = renderHook(() => useAuth(), { wrapper })

    expect(result.current.user).toEqual({ id: 1, name: 'Test User' })
    expect(result.current.login).toBeDefined()
  })

  it('should throw error when used outside provider', () => {
    // Capturer l'erreur
    const { result } = renderHook(() => useAuth())
    expect(result.error).toEqual(
      Error('useAuth must be used within AuthProvider')
    )
  })
})

Astuce : paramètre wrapper autour du Provider.

Hooks async (fetch)

Exemple de hook de fetch :

// hooks/useFetch.ts
import { useState, useEffect } from 'react'

export function useFetch<T>(url: string) {
  const [data, setData] = useState<T | null>(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState<Error | null>(null)

  useEffect(() => {
    const fetchData = async () => {
      try {
        setLoading(true)
        const response = await fetch(url)
        if (!response.ok) throw new Error('Network error')
        const json = await response.json()
        setData(json)
      } catch (err) {
        setError(err as Error)
      } finally {
        setLoading(false)
      }
    }

    fetchData()
  }, [url])

  return { data, loading, error }
}

Mocker fetch et attendre les mises à jour :

// hooks/useFetch.test.ts
import { renderHook, waitFor } from '@testing-library/react'
import { useFetch } from './useFetch'

describe('useFetch', () => {
  beforeEach(() => {
    // Réinitialiser le mock fetch avant chaque test
    jest.resetAllMocks()
  })

  it('should fetch data successfully', async () => {
    const mockData = { id: 1, title: 'Test' }

    global.fetch = jest.fn(() =>
      Promise.resolve({
        ok: true,
        json: () => Promise.resolve(mockData),
      })
    ) as jest.Mock

    const { result } = renderHook(() => useFetch('/api/data'))

    // Initial state: loading = true
    expect(result.current.loading).toBe(true)
    expect(result.current.data).toBeNull()

    // Wait for data to load
    await waitFor(() => {
      expect(result.current.loading).toBe(false)
    })

    expect(result.current.data).toEqual(mockData)
    expect(result.current.error).toBeNull()
  })

  it('should handle fetch error', async () => {
    global.fetch = jest.fn(() =>
      Promise.resolve({
        ok: false,
      })
    ) as jest.Mock

    const { result } = renderHook(() => useFetch('/api/data'))

    await waitFor(() => {
      expect(result.current.loading).toBe(false)
    })

    expect(result.current.error).toBeTruthy()
    expect(result.current.error?.message).toBe('Network error')
    expect(result.current.data).toBeNull()
  })
})

À retenir :

  • waitFor pour la fin des opérations async
  • réinitialiser les mocks dans beforeEach
  • tester succès et échec

Trois étapes : mocker les dépendances, déclencher l’état, assert. Avec ça, tout hook est testable.

Panorama des techniques de mock

Sans mock, les tests tapent API, BDD et services tiers : lent et instable. Next.js impose des mocks spécifiques — ce chapitre les détaille.

Mock du routeur Next.js (le plus courant)

Les hooks useRouter, usePathname, useSearchParams ne sont pas disponibles en test — à mocker.

Mock de useRouter (App Router) :

// __mocks__/next/navigation.ts
export const useRouter = jest.fn()
export const usePathname = jest.fn()
export const useSearchParams = jest.fn()

Dans le fichier de test :

import { useRouter } from 'next/navigation'

// Comportement du routeur
jest.mock('next/navigation', () => ({
  useRouter: jest.fn(),
  usePathname: jest.fn(),
  useSearchParams: jest.fn(),
}))

describe('NavigationComponent', () => {
  it('should navigate to home on button click', () => {
    const pushMock = jest.fn()
    ;(useRouter as jest.Mock).mockReturnValue({
      push: pushMock,
      back: jest.fn(),
      forward: jest.fn(),
    })

    render(<NavigationComponent />)

    const button = screen.getByRole('button', { name: 'Retour à l'accueil' })
    fireEvent.click(button)

    expect(pushMock).toHaveBeenCalledWith('/')
  })
})

Mock de usePathname (chemin courant) :

import { usePathname } from 'next/navigation'

jest.mock('next/navigation', () => ({
  usePathname: jest.fn(),
}))

describe('HeaderComponent', () => {
  it('should highlight active nav item', () => {
    ;(usePathname as jest.Mock).mockReturnValue('/about')

    render(<Header />)

    const aboutLink = screen.getByRole('link', { name: 'À propos' })
    expect(aboutLink).toHaveClass('active')
  })
})

Mock du composant Image

next/image échoue en test car il dépend de l’optimisation d’images Next.js.

Option 1 : remplacer par une balise img :

// __mocks__/next/image.tsx
const Image = ({ src, alt }: { src: string; alt: string }) => {
  return <img src={src} alt={alt} />
}

export default Image

Option 2 : mock global dans jest.config.ts :

const config: Config = {
  // … autres réglages
  moduleNameMapper: {
    '^next/image$': '<rootDir>/__mocks__/next/image.tsx',
  },
}

Tous les next/image pointent vers le mock.

Mock des requêtes API (3 méthodes)

Méthode 1 : mock global de fetch (le plus simple) :

global.fetch = jest.fn(() =>
  Promise.resolve({
    ok: true,
    json: () => Promise.resolve({ data: 'mock data' }),
  })
) as jest.Mock

Méthode 2 : MSW (Mock Service Worker) (plus puissant) :

npm install -D msw
// mocks/handlers.ts
import { http, HttpResponse } from 'msw'

export const handlers = [
  http.get('/api/posts', () => {
    return HttpResponse.json([
      { id: 1, title: 'Test Post' },
    ])
  }),

  http.post('/api/login', async ({ request }) => {
    const { email } = await request.json()
    if (email === '[email protected]') {
      return HttpResponse.json({ success: true })
    }
    return HttpResponse.json({ error: 'Invalid email' }, { status: 400 })
  }),
]
// mocks/server.ts
import { setupServer } from 'msw/node'
import { handlers } from './handlers'

export const server = setupServer(...handlers)

Démarrer le serveur mock dans jest.setup.ts :

import '@testing-library/jest-dom'
import { server } from './mocks/server'

// Démarrer le serveur mock avant les tests
beforeAll(() => server.listen())

// Réinitialiser les handlers après chaque test
afterEach(() => server.resetHandlers())

// Fermer le serveur à la fin
afterAll(() => server.close())

MSW intercepte toutes les requêtes sans réécrire global.fetch à chaque test.

Méthode 3 : mock axios (si vous utilisez axios) :

npm install -D axios-mock-adapter
import axios from 'axios'
import MockAdapter from 'axios-mock-adapter'

const mock = new MockAdapter(axios)

describe('API Test', () => {
  afterEach(() => {
    mock.reset()
  })

  it('should fetch posts', async () => {
    mock.onGet('/api/posts').reply(200, [{ id: 1, title: 'Test' }])

    const response = await axios.get('/api/posts')
    expect(response.data).toHaveLength(1)
  })
})

Mock des variables d’environnement

Les variables d’environnement Next.js doivent aussi être mockées en test.

Méthode 1 : définir process.env :

describe('Config Test', () => {
  const originalEnv = process.env

  beforeEach(() => {
    jest.resetModules()
    process.env = { ...originalEnv }
  })

  afterEach(() => {
    process.env = originalEnv
  })

  it('should use API URL from env', () => {
    process.env.NEXT_PUBLIC_API_URL = 'https://test-api.com'

    const { getApiUrl } = require('@/lib/config')
    expect(getApiUrl()).toBe('https://test-api.com')
  })
})

Méthode 2 : fichier .env.test :

Créez .env.test ; next/jest le charge automatiquement :

NEXT_PUBLIC_API_URL=https://test-api.com
DATABASE_URL=postgresql://test:test@localhost:5432/test

Mock de modules tiers (exemple Prisma)

Avec Prisma, évitez une vraie base en test.

Mock du client Prisma :

// __mocks__/prisma.ts
export const prisma = {
  user: {
    findMany: jest.fn(),
    findUnique: jest.fn(),
    create: jest.fn(),
    update: jest.fn(),
    delete: jest.fn(),
  },
  post: {
    findMany: jest.fn(),
    create: jest.fn(),
  },
}

Dans les tests :

import { prisma } from '@/lib/prisma'

jest.mock('@/lib/prisma', () => ({
  prisma: {
    user: {
      findUnique: jest.fn(),
    },
  },
}))

describe('getUserById', () => {
  it('should return user', async () => {
    const mockUser = { id: 1, name: 'Test User' }
    ;(prisma.user.findUnique as jest.Mock).mockResolvedValue(mockUser)

    const result = await getUserById(1)
    expect(result).toEqual(mockUser)
    expect(prisma.user.findUnique).toHaveBeenCalledWith({
      where: { id: 1 },
    })
  })
})

Erreurs de mock fréquentes

Problème 1 : Cannot find module 'next/router'

Cause : module de routage non mocké.

Solution : jest.mock('next/navigation') en tête du fichier de test.

Problème 2 : le mock ne s’applique pas

Cause : jest.mock mal placé — en haut du fichier, après les imports.

import { useRouter } from 'next/navigation'

// Mock ici obligatoirement
jest.mock('next/navigation')

describe('Test', () => {
  // ...
})

Problème 3 : variables d’environnement introuvables

Cause : .env non chargé.

Solution : jest.config.ts enveloppé par next/jest, qui charge les variables.

Avec ces mocks, la plupart des spécificités Next.js sont testables.

Dépannage des problèmes courants

Les erreurs à la config Jest sont la norme ; bonne nouvelle : 90 % se ressemblent et ont des solutions connues.

Erreur 1 : Cannot use import statement outside a module

Message complet :

SyntaxError: Cannot use import statement outside a module

Cause : Jest ne gère pas les ES Modules par défaut ; votre code ou une dépendance utilise import/export.

Solution : ajoutez dans jest.config.ts :

const config: Config = {
  // … autres réglages
  extensionsToTreatAsEsm: ['.ts', '.tsx'],
  transformIgnorePatterns: [
    'node_modules/(?!(module-that-uses-esm)/)',
  ],
}

Si un paquet npm (ex. nanoid, uuid) est en cause, ajoutez-le aux exceptions de transformIgnorePatterns :

transformIgnorePatterns: [
  'node_modules/(?!(nanoid|uuid)/)',
]

Erreur 2 : Unexpected token ‘export’

Cause : fichier non transformé par Jest, comme ci-dessus.

Solution : vérifiez transform et transformIgnorePatterns ; enveloppez avec next/jest :

import nextJest from 'next/jest'

const createJestConfig = nextJest({ dir: './' })

export default createJestConfig(config)

next/jest gère TypeScript et JSX.

Erreur 3 : Cannot find module ’@/components/…’

Cause : Jest ne reconnaît pas l’alias @/.

Solution : moduleNameMapper dans jest.config.ts :

moduleNameMapper: {
  '^@/components/(.*)$': '<rootDir>/components/$1',
  '^@/lib/(.*)$': '<rootDir>/lib/$1',
  '^@/(.*)$': '<rootDir>/$1',
}

Alignez avec paths dans tsconfig.json.

Erreur 4 : Objects are not valid as a React child

Cause : Server Component async — non supporté par Jest.

Solution :

  1. extraire la logique async en fonctions pures
  2. ou tests E2E (Playwright, Cypress)

Ne forcez pas Jest sur tous les Server Components.

Erreur 5 : avertissement act(…)

Message complet :

Warning: An update to Component inside a test was not wrapped in act(...).

Cause : mise à jour d’état async (useEffect, setTimeout) sans attente.

Solution : enveloppez les opérations async avec waitFor ou act :

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

it('should update state', async () => {
  render(<Component />)

  await waitFor(() => {
    expect(screen.getByText('Updated')).toBeInTheDocument()
  })
})

Ou avec act :

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

it('should trigger callback', async () => {
  await act(async () => {
    render(<Component />)
  })
})

Tests lents ? Optimisations

  1. Paralléliser :
{
  "scripts": {
    "test": "jest --maxWorkers=4"
  }
}
  1. Fichiers modifiés uniquement :
npm test -- --onlyChanged
  1. Désactiver la couverture (debug) :
{
  "scripts": {
    "test:fast": "jest --no-coverage"
  }
}
  1. test.skip pour les tests lents :
describe.skip('Slow Tests', () => {
  // ces tests sont ignorés
})

Checklist de diagnostic

En cas d’erreur, vérifiez dans cet ordre :

  1. jest.config.ts enveloppé par next/jest ?
  2. jest.setup.ts importe @testing-library/jest-dom ?
  3. ✅ alias de chemins alignés avec tsconfig.json ?
  4. ✅ modules mockés (next/navigation, next/image) ?
  5. ✅ opérations async avec waitFor ou act ?
  6. ✅ versions compatibles (surtout React 19 et Jest) ?

Cette checklist règle la majorité des cas.

Conclusion

La config est fastidieuse, mais une fois en place, la qualité du code monte nettement.

Au début, j’y voyais une perte de temps — 5 min pour une feature, 10 min pour les tests. Puis le refactor et les correctifs sont devenus rassurants : tout vert, on avance ; une ligne rouge, on sait quoi a cassé.

Conseils :

  • Ne attendez pas que le projet grossisse : un test par nouvelle feature, dès maintenant.
  • Pas besoin de 100 % de couverture : logique métier et zones fragiles en priorité.
  • Server Components difficiles : extraire la logique ou E2E, sans forcer Jest.
  • Erreur ? Checklist du chapitre 5 — dans la plupart des cas, ça suffit.

Gardez les fichiers de config de cet article : au prochain projet, copiez-collez et l’environnement tourne en dix minutes. Plus de tests, moins de bugs.

Blocage sur la config ou l’écriture des tests ? Commentez — j’ai passé par les mêmes galères, je peux aider.

Processus complet de configuration de l'environnement de test Jest pour Next.js

Étapes détaillées pour configurer Next.js 15 + Jest + React Testing Library depuis zéro

⏱️ Estimated time: 15 min

  1. 1

    Step 1: Installer les paquets de test

    Installez Jest et la suite React Testing Library :

    • npm install -D jest jest-environment-jsdom
    • npm install -D @testing-library/react @testing-library/dom @testing-library/jest-dom
    • npm install -D ts-node @types/jest (projet TypeScript)

    Rôle des paquets :
    • jest : cœur du framework de test
    • jest-environment-jsdom : simule le DOM du navigateur
    • @testing-library/react : utilitaires de test React
    • @testing-library/jest-dom : assertions étendues (ex. toBeInTheDocument)

    Après installation, ne lancez pas les tests : terminez d'abord les fichiers de config.
  2. 2

    Step 2: Créer le fichier de configuration Jest

    Créez jest.config.ts à la racine, avec next/jest pour gérer les spécificités Next.js :

    ```typescript
    import type { Config } from 'jest'
    import nextJest from 'next/jest'

    const createJestConfig = nextJest({ dir: './' })

    const config: Config = {
    coverageProvider: 'v8',
    testEnvironment: 'jsdom',
    setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
    // alias de chemins : moduleNameMapper
    moduleNameMapper: {
    '^@/components/(.*)$': '<rootDir>/components/$1',
    '^@/lib/(.*)$': '<rootDir>/lib/$1',
    },
    }

    export default createJestConfig(config)
    ```

    next/jest gère : mock CSS/images, variables d'environnement, transformation TypeScript, exclusion de node_modules.
  3. 3

    Step 3: Créer le fichier de démarrage Jest

    Créez jest.setup.ts à la racine et importez les extensions Jest DOM :

    ```typescript
    import '@testing-library/jest-dom'
    ```

    Cette ligne active les assertions étendues :
    • expect(element).toBeInTheDocument()
    • expect(element).toHaveClass('active')
    • expect(element).toBeVisible()
    • expect(element).toHaveTextContent('text')

    Sans cet import, ces méthodes lèvent une erreur « not a function ».
  4. 4

    Step 4: Configurer les scripts de test

    Ajoutez les scripts de test dans package.json :

    ```json
    {
    "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage"
    }
    }
    ```

    Trois commandes :
    • npm test : tous les tests (CI/CD)
    • npm run test:watch : mode watch en développement
    • npm run test:coverage : rapport de couverture
  5. 5

    Step 5: Créer un test exemple pour valider la config

    Créez __tests__/example.test.ts pour valider l'environnement :

    ```typescript
    describe('Example Test', () => {
    it('should pass basic assertion', () => {
    expect(1 + 1).toBe(2)
    })
    })
    ```

    Lancez npm test : un PASS vert confirme la config.

    En cas d'erreur, vérifiez :
    1. jest.config.ts enveloppé par createJestConfig
    2. jest.setup.ts correctement importé
    3. scripts package.json corrects
    4. alias de chemins alignés avec tsconfig.json

FAQ

Pourquoi envelopper la configuration avec next/jest ?
next/jest est l'outil officiel Next.js pour Jest. Il gère automatiquement :

• le mock des CSS et images (évite les erreurs en test)
• le chargement des variables .env
• la transformation TypeScript et JSX
• l'exclusion de node_modules et .next
• les règles du compilateur Next.js

Sans next/jest, tout cela se configure à la main — long et fragile. La doc recommande createJestConfig pour envelopper la configuration.
Peut-on tester les Server Components avec Jest ?
Partiellement, avec des limites :

• Server Components synchrones : testables normalement
• Server Components async : non supportés par Jest (erreur « Objects are not valid as a React child »)

Approche recommandée :
1. extraire la logique async en fonctions pures et tester la récupération de données
2. garder des Server Components simples, sans logique complexe
3. compléter avec Playwright ou Cypress en E2E

Ne forcez pas Jest sur tous les Server Components — le bon outil compte plus.
Comment mocker useRouter de Next.js en test ?
Les hooks de routage App Router doivent être mockés :

```typescript
import { useRouter } from 'next/navigation'

jest.mock('next/navigation', () => ({
useRouter: jest.fn(),
usePathname: jest.fn(),
useSearchParams: jest.fn(),
}))

const pushMock = jest.fn()
;(useRouter as jest.Mock).mockReturnValue({
push: pushMock,
back: jest.fn(),
forward: jest.fn(),
})
```

jest.mock doit être en tête du fichier (après les imports), pas dans describe ou it.
Pourquoi l'erreur « Cannot use import statement outside a module » ?
Jest ne gère pas les ES Modules par défaut. Deux solutions :

Solution 1 : transformIgnorePatterns (recommandé)
```typescript
const config: Config = {
extensionsToTreatAsEsm: ['.ts', '.tsx'],
transformIgnorePatterns: [
'node_modules/(?!(nanoid|uuid)/)',
],
}
```

Solution 2 : envelopper correctement avec next/jest
```typescript
import nextJest from 'next/jest'
const createJestConfig = nextJest({ dir: './' })
export default createJestConfig(config)
```

Dans 90 % des cas, un paquet npm en ESM — ajoutez son nom aux exceptions de transformIgnorePatterns.
Comment mocker les requêtes API ? Quelle méthode privilégier ?
Trois méthodes, par complexité croissante :

Méthode 1 : mock global de fetch (simple, petits projets)
```typescript
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({ data: 'test' }),
})
) as jest.Mock
```

Méthode 2 : MSW (recommandé, projets moyens/grands)
• intercepte toutes les requêtes réseau
• logique requête/réponse complexe
• pas de mock à réécrire dans chaque test
• npm install -D msw

Méthode 3 : axios-mock-adapter (si axios)
• mock dédié axios, API simple

MSW se rapproche du réseau réel et se réutilise bien en équipe.
Tests lents : comment optimiser ?
Quatre optimisations utiles :

1. paralléliser (le plus efficace)
```json
{ "scripts": { "test": "jest --maxWorkers=4" } }
```

2. fichiers modifiés uniquement
```bash
npm test -- --onlyChanged
```

3. désactiver la couverture (debug)
```json
{ "scripts": { "test:fast": "jest --no-coverage" } }
```

4. ignorer les tests lents
```typescript
describe.skip('Slow E2E Tests', () => {
// ces tests sont ignorés
})
```

En dev : --onlyChanged et --no-coverage ; en CI/CD : suite complète.
Alias @/ configuré mais les tests échouent encore ?
Alignez jest.config.ts et tsconfig.json :

tsconfig.json (ou jsconfig.json) :
```json
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/components/*": ["components/*"],
"@/lib/*": ["lib/*"]
}
}
}
```

jest.config.ts :
```typescript
moduleNameMapper: {
'^@/components/(.*)$': '<rootDir>/components/$1',
'^@/lib/(.*)$': '<rootDir>/lib/$1',
}
```

À retenir :
1. tsconfig : chemins relatifs (sans <rootDir>)
2. jest : chemins avec <rootDir>
3. mêmes wildcards (.*)

Relancez les tests après modification.

15 min de lecture · Publié le: 7 janv. 2026 · Mis à jour le: 27 juil. 2026

Commentaires

Connectez-vous avec GitHub pour laisser un commentaire

Easton BlogEaston Blog