Tester son code, c'est un peu comme mettre une ceinture de sécurité : on espère ne jamais en avoir besoin, mais le jour où ça craque, on est bien content de l'avoir mise. Pourtant, beaucoup de projets Next.js tournent en production sans la moindre suite de tests. Dans cet article, je vous montre comment mettre en place Jest et React Testing Library sur un projet Next.js App Router avec TypeScript — et surtout comment écrire des tests qui ont vraiment de la valeur.
Pourquoi tester ses composants React ?
Avant de plonger dans le code, posons-nous la question : qu'est-ce qu'on cherche à tester, exactement ?
L'idée derrière React Testing Library est de tester le comportement visible par l'utilisateur, pas les détails d'implémentation. On ne teste pas si un useState a la bonne valeur interne — on teste si l'utilisateur voit ce qu'il doit voir et si les interactions fonctionnent comme prévu. Cette philosophie, résumée dans la devise de la bibliothèque ("The more your tests resemble the way your software is used, the more confidence they can give you"), change radicalement la façon d'aborder les tests frontend.
Concrètement, des tests bien écrits permettent de :
- Refactoriser en confiance : vous pouvez réorganiser votre code sans craindre de casser un comportement existant.
- Documenter le comportement attendu : un test bien nommé vaut parfois mieux qu'un commentaire.
- Détecter les régressions : quand une PR casse quelque chose, votre CI vous le signale avant la mise en prod.
Installation et configuration sur Next.js App Router
Next.js intègre Jest via create-next-app, mais si vous l'ajoutez sur un projet existant, voici la marche à suivre.
Installer les dépendances
npm install --save-dev jest jest-environment-jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-event ts-jest
Configurer Jest
Créez un fichier jest.config.ts à la racine du projet :
import type { Config } from 'jest';
import nextJest from 'next/jest.js';
const createJestConfig = nextJest({
// Chemin vers votre app Next.js pour charger next.config.js et .env
dir: './',
});
const config: Config = {
coverageProvider: 'v8',
testEnvironment: 'jsdom',
setupFilesAfterFramework: ['<rootDir>/jest.setup.ts'],
};
export default createJestConfig(config);
Note :
next/jests'occupe automatiquement de la transpilation TypeScript, des alias de chemins (@/) et des imports de fichiers statiques. Vous n'avez pas besoin de configurer Babel outs-jestmanuellement.
Fichier de setup
Créez jest.setup.ts :
import '@testing-library/jest-dom';
Cet import ajoute les matchers personnalisés comme toBeInTheDocument(), toHaveTextContent(), toBeVisible(), etc.
Script npm
Ajoutez dans package.json :
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
}
Votre premier test : un composant simple
Prenons un composant Button classique :
// src/components/Button.tsx
interface ButtonProps {
label: string;
onClick: () => void;
disabled?: boolean;
}
export function Button({ label, onClick, disabled = false }: ButtonProps) {
return (
<button onClick={onClick} disabled={disabled} className="btn-primary">
{label}
</button>
);
}
Son test :
// src/components/Button.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Button } from './Button';
describe('Button', () => {
it('affiche le label correctement', () => {
render(<Button label="Envoyer" onClick={() => {}} />);
expect(screen.getByRole('button', { name: 'Envoyer' })).toBeInTheDocument();
});
it("appelle onClick quand on clique dessus", async () => {
const handleClick = jest.fn();
render(<Button label="Envoyer" onClick={handleClick} />);
await userEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('est désactivé quand disabled=true', () => {
render(<Button label="Envoyer" onClick={() => {}} disabled />);
expect(screen.getByRole('button')).toBeDisabled();
});
});
Quelques points à noter :
- On utilise
getByRoleplutôt quegetByTestId— cela teste aussi l'accessibilité. userEventsimule de vraies interactions utilisateur (événements souris, clavier) contrairement àfireEvent.- Les assertions
jest.fn()permettent de vérifier qu'une fonction a bien été appelée.
Tester un formulaire avec état
Les formulaires sont souvent au cœur des bugs. Voici comment tester un composant avec useState :
// src/components/SearchForm.tsx
'use client';
import { useState } from 'react';
interface SearchFormProps {
onSearch: (query: string) => void;
}
export function SearchForm({ onSearch }: SearchFormProps) {
const [query, setQuery] = useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (query.trim()) onSearch(query);
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="Rechercher..."
value={query}
onChange={(e) => setQuery(e.target.value)}
aria-label="Terme de recherche"
/>
<button type="submit">Rechercher</button>
</form>
);
}
// src/components/SearchForm.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { SearchForm } from './SearchForm';
describe('SearchForm', () => {
it("appelle onSearch avec la valeur saisie", async () => {
const handleSearch = jest.fn();
render(<SearchForm onSearch={handleSearch} />);
await userEvent.type(screen.getByLabelText('Terme de recherche'), 'Next.js');
await userEvent.click(screen.getByRole('button', { name: 'Rechercher' }));
expect(handleSearch).toHaveBeenCalledWith('Next.js');
});
it("ne déclenche pas onSearch si le champ est vide", async () => {
const handleSearch = jest.fn();
render(<SearchForm onSearch={handleSearch} />);
await userEvent.click(screen.getByRole('button', { name: 'Rechercher' }));
expect(handleSearch).not.toHaveBeenCalled();
});
});
Mocker les dépendances Next.js
Next.js expose plusieurs hooks (useRouter, usePathname, useSearchParams) qui ne fonctionnent pas nativement dans un environnement Jest. Il faut les mocker.
Mocker useRouter
// Dans votre test
jest.mock('next/navigation', () => ({
useRouter: () => ({
push: jest.fn(),
replace: jest.fn(),
back: jest.fn(),
}),
usePathname: () => '/test',
useSearchParams: () => new URLSearchParams(),
}));
Exemple avec une redirection
// src/components/LogoutButton.tsx
'use client';
import { useRouter } from 'next/navigation';
export function LogoutButton() {
const router = useRouter();
const handleLogout = () => {
// Logique de déconnexion...
router.push('/login');
};
return <button onClick={handleLogout}>Se déconnecter</button>;
}
// src/components/LogoutButton.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LogoutButton } from './LogoutButton';
const mockPush = jest.fn();
jest.mock('next/navigation', () => ({
useRouter: () => ({ push: mockPush }),
}));
describe('LogoutButton', () => {
beforeEach(() => {
mockPush.mockClear();
});
it('redirige vers /login au clic', async () => {
render(<LogoutButton />);
await userEvent.click(screen.getByRole('button', { name: 'Se déconnecter' }));
expect(mockPush).toHaveBeenCalledWith('/login');
});
});
Bonnes pratiques à retenir
Après plusieurs projets en production, voici les règles qui m'ont le plus aidé :
Nommez vos tests comme des specs. Un test nommé "affiche un message d'erreur quand l'email est invalide" est infiniment plus utile que "test 3". Quand un test échoue en CI, vous comprenez immédiatement ce qui est cassé.
Préférez getByRole et getByLabelText. Ces sélecteurs reflètent comment un utilisateur (humain ou lecteur d'écran) interagit avec l'interface. getByTestId doit rester un dernier recours.
Un test = un comportement. Ne testez pas cinq choses dans le même it. Des tests granulaires sont plus faciles à débugger.
Ne testez pas l'implémentation. Si vous refactorisez un useState en useReducer sans changer le comportement visible, vos tests ne devraient pas bouger.
Évitez les snapshots automatiques pour les composants complexes. Ils ont tendance à casser pour n'importe quelle modification de markup, même anodine, et finissent par être mis à jour sans réflexion.
Conclusion
Mettre en place Jest et React Testing Library sur un projet Next.js n'est pas très compliqué — la vraie difficulté est d'écrire des tests qui apportent de la valeur sans devenir un fardeau à maintenir. En se concentrant sur le comportement utilisateur plutôt que sur les détails d'implémentation, on obtient une suite de tests qui donne confiance lors des refactorisations et des montées de version.
Si vous démarrez un nouveau projet Next.js, intégrez les tests dès le début : c'est beaucoup plus facile que de les ajouter après coup sur une codebase de 50 000 lignes.
Vous avez un projet web à développer ou à refondre ? N'hésitez pas à me contacter pour en discuter — je serai ravi d'échanger sur vos besoins.