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

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_moduleset.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 foisnpm 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 :
render()pour monter le composantscreen.getByXxx()pour cibler les éléments (rôle, texte, placeholder…)fireEventpour simuler l’utilisateurexpect()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 :
renderHookmonte le hookactenveloppe les mises à jour d’état (règle React)result.currentlit 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 :
waitForpour 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 :
- extraire la logique async en fonctions pures
- 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
- Paralléliser :
{
"scripts": {
"test": "jest --maxWorkers=4"
}
}
- Fichiers modifiés uniquement :
npm test -- --onlyChanged
- Désactiver la couverture (debug) :
{
"scripts": {
"test:fast": "jest --no-coverage"
}
}
test.skippour les tests lents :
describe.skip('Slow Tests', () => {
// ces tests sont ignorés
})
Checklist de diagnostic
En cas d’erreur, vérifiez dans cet ordre :
- ✅
jest.config.tsenveloppé parnext/jest? - ✅
jest.setup.tsimporte@testing-library/jest-dom? - ✅ alias de chemins alignés avec
tsconfig.json? - ✅ modules mockés (
next/navigation,next/image) ? - ✅ opérations async avec
waitForouact? - ✅ 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
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
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
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
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
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 ?
• 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 ?
• 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 ?
```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 » ?
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 ?
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 ?
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 ?
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
Guide complet Next.js
Si vous arrivez depuis la recherche, le plus rapide est de passer à l’article précédent ou suivant de cette série.
Précédent
Guide complet Next.js Error Boundary : 5 techniques pour gérer élégamment les erreurs runtime
Maîtrisez Error Boundary dans Next.js : error.tsx, gestion globale, cas particuliers des Server Components et mécanismes de récupération — évitez l'écran blanc et améliorez l'expérience utilisateur.
Partie 34 sur 51
Suivant
Tests E2E Next.js : guide pratique Playwright pour l'automatisation
Retour d'expérience complet du test manuel aux tests E2E automatisés : configuration Playwright, Page Object Model, tests API et intégration CI/CD pour Next.js.
Partie 36 sur 51



Commentaires
Connectez-vous avec GitHub pour laisser un commentaire