Vitest et React Testing Library avec Next.js 15 : Le Guide Complet des Tests Unitaires en 2026

Testez avec confiance. Vitest est le framework de tests ultra-rapide alimenté par Vite qui a remplacé Jest dans la plupart des projets modernes en 2026. Combiné avec React Testing Library, il offre une expérience de test intuitive, rapide et fiable. Dans ce tutoriel, vous apprendrez à tester chaque couche de votre application Next.js 15.
Ce que vous apprendrez
À la fin de ce tutoriel, vous saurez :
- Configurer Vitest dans un projet Next.js 15 App Router
- Écrire des tests de composants avec React Testing Library
- Tester les hooks personnalisés avec
renderHook - Mocker les Server Components et les Server Actions
- Tester les API Routes (Route Handlers)
- Mesurer et améliorer la couverture de code
- Intégrer les tests dans un pipeline CI/CD
- Appliquer les bonnes pratiques pour des tests maintenables
Prérequis
Avant de commencer, assurez-vous d'avoir :
- Node.js 20+ installé (
node --version) - De l'expérience en TypeScript et React
- Une connaissance de Next.js 15 (App Router, Server Components)
- Un éditeur de code — VS Code ou Cursor recommandé
- Des notions de base sur les tests logiciels (assertions, mocks)
Pourquoi Vitest plutôt que Jest ?
En 2026, Vitest est devenu le standard pour les tests dans l'écosystème JavaScript. Voici pourquoi :
| Critère | Jest | Vitest |
|---|---|---|
| Vitesse | Lent (transformation CJS) | Ultra-rapide (ESM natif via Vite) |
| Configuration | Complexe avec Next.js | Minimale avec next/vitest |
| ESM | Support partiel | Support natif complet |
| Hot reload | Non | Mode watch intelligent |
| API | Propriétaire | Compatible Jest (migration facile) |
| TypeScript | Nécessite ts-jest | Support natif |
L'API de Vitest est intentionnellement compatible avec Jest, ce qui signifie que si vous connaissez Jest, vous connaissez déjà Vitest.
Étape 1 : Initialiser le projet Next.js
Créez un nouveau projet Next.js 15 :
npx create-next-app@latest my-tested-app --typescript --tailwind --app --src-dir
cd my-tested-appVérifiez que tout fonctionne :
npm run devÉtape 2 : Installer Vitest et React Testing Library
Installez les dépendances de test :
npm install -D vitest @vitejs/plugin-react @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdomVoici ce que fait chaque paquet :
| Paquet | Rôle |
|---|---|
vitest | Framework de tests |
@vitejs/plugin-react | Support JSX/TSX dans Vitest |
@testing-library/react | Utilitaires pour tester les composants React |
@testing-library/jest-dom | Matchers DOM personnalisés (toBeInTheDocument, etc.) |
@testing-library/user-event | Simulation réaliste des interactions utilisateur |
jsdom | Environnement DOM pour Node.js |
Étape 3 : Configurer Vitest
Créez le fichier vitest.config.ts à la racine du projet :
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
import path from "path";
export default defineConfig({
plugins: [react()],
test: {
environment: "jsdom",
globals: true,
setupFiles: ["./src/test/setup.ts"],
include: ["src/**/*.{test,spec}.{ts,tsx}"],
coverage: {
provider: "v8",
reporter: ["text", "json", "html"],
include: ["src/**/*.{ts,tsx}"],
exclude: [
"src/**/*.{test,spec}.{ts,tsx}",
"src/**/*.d.ts",
"src/test/**",
],
},
},
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
});Créez le fichier de setup src/test/setup.ts :
import "@testing-library/jest-dom/vitest";
import { cleanup } from "@testing-library/react";
import { afterEach } from "vitest";
// Nettoyer automatiquement après chaque test
afterEach(() => {
cleanup();
});Ajoutez les scripts de test dans package.json :
{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage",
"test:ui": "vitest --ui"
}
}Pour le mode UI interactif (optionnel mais très utile) :
npm install -D @vitest/uiÉtape 4 : Configurer TypeScript pour les tests
Ajoutez les types Vitest dans tsconfig.json :
{
"compilerOptions": {
"types": ["vitest/globals", "@testing-library/jest-dom"]
}
}Cela permet d'utiliser describe, it, expect et les matchers de jest-dom sans imports explicites.
Étape 5 : Votre premier test de composant
Créons un composant simple à tester. Créez src/components/Counter.tsx :
"use client";
import { useState } from "react";
interface CounterProps {
initialCount?: number;
step?: number;
}
export function Counter({ initialCount = 0, step = 1 }: CounterProps) {
const [count, setCount] = useState(initialCount);
return (
<div>
<p data-testid="count-display">Compteur : {count}</p>
<button onClick={() => setCount((c) => c + step)}>Incrémenter</button>
<button onClick={() => setCount((c) => c - step)}>Décrémenter</button>
<button onClick={() => setCount(0)}>Réinitialiser</button>
</div>
);
}Maintenant, écrivez le test dans src/components/__tests__/Counter.test.tsx :
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { Counter } from "../Counter";
describe("Counter", () => {
it("affiche le compteur initial à 0", () => {
render(<Counter />);
expect(screen.getByTestId("count-display")).toHaveTextContent(
"Compteur : 0"
);
});
it("accepte une valeur initiale personnalisée", () => {
render(<Counter initialCount={10} />);
expect(screen.getByTestId("count-display")).toHaveTextContent(
"Compteur : 10"
);
});
it("incrémente le compteur au clic", async () => {
const user = userEvent.setup();
render(<Counter />);
await user.click(screen.getByText("Incrémenter"));
expect(screen.getByTestId("count-display")).toHaveTextContent(
"Compteur : 1"
);
});
it("décrémente le compteur au clic", async () => {
const user = userEvent.setup();
render(<Counter initialCount={5} />);
await user.click(screen.getByText("Décrémenter"));
expect(screen.getByTestId("count-display")).toHaveTextContent(
"Compteur : 4"
);
});
it("réinitialise le compteur à zéro", async () => {
const user = userEvent.setup();
render(<Counter initialCount={42} />);
await user.click(screen.getByText("Réinitialiser"));
expect(screen.getByTestId("count-display")).toHaveTextContent(
"Compteur : 0"
);
});
it("utilise le pas personnalisé", async () => {
const user = userEvent.setup();
render(<Counter step={5} />);
await user.click(screen.getByText("Incrémenter"));
expect(screen.getByTestId("count-display")).toHaveTextContent(
"Compteur : 5"
);
await user.click(screen.getByText("Incrémenter"));
expect(screen.getByTestId("count-display")).toHaveTextContent(
"Compteur : 10"
);
});
});Lancez le test :
npm testVous devriez voir tous les tests passer en vert.
Étape 6 : Tester les composants avec formulaires
Les formulaires sont au cœur de la plupart des applications. Créez src/components/LoginForm.tsx :
"use client";
import { useState } from "react";
interface LoginFormProps {
onSubmit: (data: { email: string; password: string }) => Promise<void>;
}
export function LoginForm({ onSubmit }: LoginFormProps) {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
if (!email.includes("@")) {
setError("Adresse email invalide");
return;
}
if (password.length < 8) {
setError("Le mot de passe doit contenir au moins 8 caractères");
return;
}
setIsLoading(true);
try {
await onSubmit({ email, password });
} catch (err) {
setError("Erreur de connexion. Veuillez réessayer.");
} finally {
setIsLoading(false);
}
};
return (
<form onSubmit={handleSubmit} aria-label="Formulaire de connexion">
{error && (
<div role="alert" className="text-red-500">
{error}
</div>
)}
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
<label htmlFor="password">Mot de passe</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
<button type="submit" disabled={isLoading}>
{isLoading ? "Connexion en cours..." : "Se connecter"}
</button>
</form>
);
}Testez-le dans src/components/__tests__/LoginForm.test.tsx :
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { vi } from "vitest";
import { LoginForm } from "../LoginForm";
describe("LoginForm", () => {
const mockOnSubmit = vi.fn();
beforeEach(() => {
mockOnSubmit.mockReset();
});
it("rend le formulaire avec tous les champs", () => {
render(<LoginForm onSubmit={mockOnSubmit} />);
expect(screen.getByLabelText("Email")).toBeInTheDocument();
expect(screen.getByLabelText("Mot de passe")).toBeInTheDocument();
expect(screen.getByText("Se connecter")).toBeInTheDocument();
});
it("affiche une erreur pour un email invalide", async () => {
const user = userEvent.setup();
render(<LoginForm onSubmit={mockOnSubmit} />);
await user.type(screen.getByLabelText("Email"), "invalid-email");
await user.type(screen.getByLabelText("Mot de passe"), "password123");
await user.click(screen.getByText("Se connecter"));
expect(screen.getByRole("alert")).toHaveTextContent(
"Adresse email invalide"
);
expect(mockOnSubmit).not.toHaveBeenCalled();
});
it("affiche une erreur pour un mot de passe trop court", async () => {
const user = userEvent.setup();
render(<LoginForm onSubmit={mockOnSubmit} />);
await user.type(screen.getByLabelText("Email"), "user@example.com");
await user.type(screen.getByLabelText("Mot de passe"), "short");
await user.click(screen.getByText("Se connecter"));
expect(screen.getByRole("alert")).toHaveTextContent(
"Le mot de passe doit contenir au moins 8 caractères"
);
});
it("soumet le formulaire avec des données valides", async () => {
mockOnSubmit.mockResolvedValue(undefined);
const user = userEvent.setup();
render(<LoginForm onSubmit={mockOnSubmit} />);
await user.type(screen.getByLabelText("Email"), "user@example.com");
await user.type(screen.getByLabelText("Mot de passe"), "password123");
await user.click(screen.getByText("Se connecter"));
expect(mockOnSubmit).toHaveBeenCalledWith({
email: "user@example.com",
password: "password123",
});
});
it("désactive le bouton pendant le chargement", async () => {
mockOnSubmit.mockImplementation(
() => new Promise((resolve) => setTimeout(resolve, 1000))
);
const user = userEvent.setup();
render(<LoginForm onSubmit={mockOnSubmit} />);
await user.type(screen.getByLabelText("Email"), "user@example.com");
await user.type(screen.getByLabelText("Mot de passe"), "password123");
await user.click(screen.getByText("Se connecter"));
expect(screen.getByText("Connexion en cours...")).toBeDisabled();
});
it("affiche une erreur en cas d'échec de soumission", async () => {
mockOnSubmit.mockRejectedValue(new Error("Network error"));
const user = userEvent.setup();
render(<LoginForm onSubmit={mockOnSubmit} />);
await user.type(screen.getByLabelText("Email"), "user@example.com");
await user.type(screen.getByLabelText("Mot de passe"), "password123");
await user.click(screen.getByText("Se connecter"));
expect(
await screen.findByText("Erreur de connexion. Veuillez réessayer.")
).toBeInTheDocument();
});
});Étape 7 : Tester les hooks personnalisés
React Testing Library fournit renderHook pour tester les hooks isolément. Créez src/hooks/useLocalStorage.ts :
"use client";
import { useState, useEffect } from "react";
export function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
if (typeof window === "undefined") return initialValue;
try {
const item = window.localStorage.getItem(key);
return item ? (JSON.parse(item) as T) : initialValue;
} catch {
return initialValue;
}
});
useEffect(() => {
try {
window.localStorage.setItem(key, JSON.stringify(storedValue));
} catch {
console.error(`Erreur lors de la sauvegarde de "${key}" dans localStorage`);
}
}, [key, storedValue]);
return [storedValue, setStoredValue] as const;
}Testez-le dans src/hooks/__tests__/useLocalStorage.test.ts :
import { renderHook, act } from "@testing-library/react";
import { useLocalStorage } from "../useLocalStorage";
describe("useLocalStorage", () => {
beforeEach(() => {
localStorage.clear();
});
it("retourne la valeur initiale quand localStorage est vide", () => {
const { result } = renderHook(() => useLocalStorage("theme", "light"));
expect(result.current[0]).toBe("light");
});
it("lit la valeur existante depuis localStorage", () => {
localStorage.setItem("theme", JSON.stringify("dark"));
const { result } = renderHook(() => useLocalStorage("theme", "light"));
expect(result.current[0]).toBe("dark");
});
it("met à jour la valeur dans localStorage", () => {
const { result } = renderHook(() => useLocalStorage("count", 0));
act(() => {
result.current[1](42);
});
expect(result.current[0]).toBe(42);
expect(JSON.parse(localStorage.getItem("count")!)).toBe(42);
});
it("gère les objets complexes", () => {
const initial = { name: "John", age: 30 };
const { result } = renderHook(() => useLocalStorage("user", initial));
act(() => {
result.current[1]({ name: "Jane", age: 25 });
});
expect(result.current[0]).toEqual({ name: "Jane", age: 25 });
});
});Étape 8 : Mocker les modules et les API
Vitest offre un système de mocking puissant. Voici les techniques les plus courantes.
Mocker un module entier
import { vi } from "vitest";
// Mocker next/navigation
vi.mock("next/navigation", () => ({
useRouter: () => ({
push: vi.fn(),
replace: vi.fn(),
back: vi.fn(),
}),
useSearchParams: () => new URLSearchParams(),
usePathname: () => "/",
}));Mocker les appels fetch
Créez src/lib/api.ts :
export async function fetchUsers() {
const response = await fetch("/api/users");
if (!response.ok) throw new Error("Erreur de récupération");
return response.json();
}Testez avec un mock de fetch :
import { vi } from "vitest";
import { fetchUsers } from "../api";
describe("fetchUsers", () => {
it("retourne les utilisateurs", async () => {
const mockUsers = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
];
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve(mockUsers),
});
const users = await fetchUsers();
expect(users).toEqual(mockUsers);
expect(fetch).toHaveBeenCalledWith("/api/users");
});
it("lance une erreur en cas d'échec", async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: false,
status: 500,
});
await expect(fetchUsers()).rejects.toThrow("Erreur de récupération");
});
});Mocker avec vi.spyOn
import * as api from "../api";
it("espionne un appel de fonction", async () => {
const spy = vi.spyOn(api, "fetchUsers").mockResolvedValue([]);
// ... votre code qui appelle fetchUsers
expect(spy).toHaveBeenCalledTimes(1);
spy.mockRestore();
});Étape 9 : Tester les composants avec données asynchrones
Créez un composant qui charge des données. src/components/UserList.tsx :
"use client";
import { useState, useEffect } from "react";
interface User {
id: number;
name: string;
email: string;
}
export function UserList() {
const [users, setUsers] = useState<User[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetch("/api/users")
.then((res) => {
if (!res.ok) throw new Error("Erreur serveur");
return res.json();
})
.then(setUsers)
.catch((err) => setError(err.message))
.finally(() => setIsLoading(false));
}, []);
if (isLoading) return <p>Chargement...</p>;
if (error) return <p role="alert">Erreur : {error}</p>;
return (
<ul aria-label="Liste des utilisateurs">
{users.map((user) => (
<li key={user.id}>
<strong>{user.name}</strong> — {user.email}
</li>
))}
</ul>
);
}Testez-le :
import { render, screen } from "@testing-library/react";
import { vi } from "vitest";
import { UserList } from "../UserList";
describe("UserList", () => {
it("affiche l'état de chargement", () => {
global.fetch = vi.fn().mockImplementation(
() => new Promise(() => {}) // Promesse qui ne se résout jamais
);
render(<UserList />);
expect(screen.getByText("Chargement...")).toBeInTheDocument();
});
it("affiche la liste des utilisateurs", async () => {
const mockUsers = [
{ id: 1, name: "Alice", email: "alice@example.com" },
{ id: 2, name: "Bob", email: "bob@example.com" },
];
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve(mockUsers),
});
render(<UserList />);
expect(await screen.findByText("Alice")).toBeInTheDocument();
expect(screen.getByText("Bob")).toBeInTheDocument();
expect(screen.getByRole("list")).toHaveAttribute(
"aria-label",
"Liste des utilisateurs"
);
});
it("affiche un message d'erreur en cas d'échec", async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: false,
status: 500,
});
render(<UserList />);
expect(
await screen.findByText("Erreur : Erreur serveur")
).toBeInTheDocument();
});
});Astuce : Utilisez findByText au lieu de getByText pour les éléments qui apparaissent de manière asynchrone. findByText attend automatiquement que l'élément soit présent dans le DOM.
Étape 10 : Tester les API Routes (Route Handlers)
Next.js 15 utilise les Route Handlers dans le répertoire app/api/. Créez src/app/api/users/route.ts :
import { NextRequest, NextResponse } from "next/server";
const users = [
{ id: 1, name: "Alice", email: "alice@example.com" },
{ id: 2, name: "Bob", email: "bob@example.com" },
];
export async function GET() {
return NextResponse.json(users);
}
export async function POST(request: NextRequest) {
const body = await request.json();
if (!body.name || !body.email) {
return NextResponse.json(
{ error: "Nom et email requis" },
{ status: 400 }
);
}
const newUser = {
id: users.length + 1,
name: body.name,
email: body.email,
};
return NextResponse.json(newUser, { status: 201 });
}Testez les Route Handlers directement :
import { GET, POST } from "@/app/api/users/route";
import { NextRequest } from "next/server";
describe("API /api/users", () => {
describe("GET", () => {
it("retourne la liste des utilisateurs", async () => {
const response = await GET();
const data = await response.json();
expect(response.status).toBe(200);
expect(data).toHaveLength(2);
expect(data[0]).toHaveProperty("name", "Alice");
});
});
describe("POST", () => {
it("crée un nouvel utilisateur", async () => {
const request = new NextRequest("http://localhost/api/users", {
method: "POST",
body: JSON.stringify({ name: "Charlie", email: "charlie@example.com" }),
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(201);
expect(data).toMatchObject({
name: "Charlie",
email: "charlie@example.com",
});
});
it("retourne 400 si les données sont incomplètes", async () => {
const request = new NextRequest("http://localhost/api/users", {
method: "POST",
body: JSON.stringify({ name: "Charlie" }),
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(400);
expect(data.error).toBe("Nom et email requis");
});
});
});Étape 11 : Tester avec des providers (Context, Theme, etc.)
La plupart des applications utilisent des providers React. Créez un utilitaire de rendu personnalisé :
// src/test/test-utils.tsx
import { ReactElement } from "react";
import { render, RenderOptions } from "@testing-library/react";
// Importez vos providers
// import { ThemeProvider } from "@/components/ThemeProvider";
// import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
function AllProviders({ children }: { children: React.ReactNode }) {
// const queryClient = new QueryClient({
// defaultOptions: { queries: { retry: false } },
// });
return (
// <QueryClientProvider client={queryClient}>
// <ThemeProvider>
<>{children}</>
// </ThemeProvider>
// </QueryClientProvider>
);
}
function customRender(
ui: ReactElement,
options?: Omit<RenderOptions, "wrapper">
) {
return render(ui, { wrapper: AllProviders, ...options });
}
// Ré-exporter tout
export * from "@testing-library/react";
export { customRender as render };Utilisez-le dans vos tests :
// Au lieu de :
import { render, screen } from "@testing-library/react";
// Utilisez :
import { render, screen } from "@/test/test-utils";Étape 12 : Snapshots et tests visuels
Vitest supporte les tests par snapshot pour détecter les changements inattendus :
import { render } from "@testing-library/react";
import { Counter } from "../Counter";
it("correspond au snapshot", () => {
const { container } = render(<Counter initialCount={5} />);
expect(container).toMatchSnapshot();
});
// Ou avec des inline snapshots (plus lisibles)
it("correspond au snapshot inline", () => {
const { container } = render(<Counter />);
expect(container.firstChild).toMatchInlineSnapshot(`
<div>
<p data-testid="count-display">Compteur : 0</p>
<button>Incrémenter</button>
<button>Décrémenter</button>
<button>Réinitialiser</button>
</div>
`);
});Attention : Les snapshots sont utiles pour détecter les régressions, mais ne les utilisez pas comme substitut aux assertions explicites. Préférez toHaveTextContent et toBeInTheDocument pour vérifier le comportement.
Étape 13 : Couverture de code
Lancez les tests avec la couverture :
npm run test:coverageVitest génère un rapport de couverture détaillé :
-----------------------|---------|----------|---------|---------|
File | % Stmts | % Branch | % Funcs | % Lines |
-----------------------|---------|----------|---------|---------|
All files | 92.3 | 85.7 | 100 | 92.3 |
components/Counter | 100 | 100 | 100 | 100 |
components/LoginForm | 95.2 | 83.3 | 100 | 95.2 |
hooks/useLocalStorage | 88.9 | 75.0 | 100 | 88.9 |
-----------------------|---------|----------|---------|---------|
Configurer les seuils de couverture
Ajoutez des seuils minimaux dans vitest.config.ts :
coverage: {
provider: "v8",
thresholds: {
statements: 80,
branches: 75,
functions: 80,
lines: 80,
},
},Si la couverture tombe en dessous de ces seuils, les tests échoueront dans la CI.
Étape 14 : Intégration CI/CD avec GitHub Actions
Créez .github/workflows/test.yml :
name: Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Installer Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
- name: Installer les dépendances
run: npm ci
- name: Lancer les tests
run: npm run test:run
- name: Vérifier la couverture
run: npm run test:coverageÉtape 15 : Bonnes pratiques et patterns avancés
1. Suivez la philosophie de Testing Library
"Plus vos tests ressemblent à la façon dont votre logiciel est utilisé, plus ils vous donnent confiance." — Kent C. Dodds
// ❌ Mauvais : tester les détails d'implémentation
expect(component.state.isOpen).toBe(true);
// ✅ Bon : tester le comportement visible
expect(screen.getByRole("dialog")).toBeVisible();2. Préférez les requêtes par rôle
// ❌ Fragile : dépend du texte exact
screen.getByText("Submit");
// ✅ Robuste : dépend du rôle accessible
screen.getByRole("button", { name: /soumettre/i });3. Utilisez userEvent au lieu de fireEvent
// ❌ fireEvent est de bas niveau
fireEvent.click(button);
// ✅ userEvent simule le comportement réel de l'utilisateur
const user = userEvent.setup();
await user.click(button);4. Organisez avec le pattern AAA
it("filtre les utilisateurs par nom", async () => {
// Arrange
const user = userEvent.setup();
render(<UserSearch users={mockUsers} />);
// Act
await user.type(screen.getByRole("searchbox"), "Alice");
// Assert
expect(screen.getByText("Alice")).toBeInTheDocument();
expect(screen.queryByText("Bob")).not.toBeInTheDocument();
});5. Évitez les tests flaky
// ❌ Timing fragile
await new Promise((resolve) => setTimeout(resolve, 1000));
expect(screen.getByText("Loaded")).toBeInTheDocument();
// ✅ Attente basée sur les assertions
expect(await screen.findByText("Loaded")).toBeInTheDocument();6. Nettoyez les mocks
afterEach(() => {
vi.restoreAllMocks();
});Dépannage
"ReferenceError: document is not defined"
Vérifiez que l'environnement est configuré à jsdom dans vitest.config.ts :
test: {
environment: "jsdom",
}"Cannot find module '@/...'"
Assurez-vous que les alias sont configurés dans vitest.config.ts :
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},Les tests sont lents
- Utilisez
--reporter=verbosepour identifier les tests lents - Évitez les
setTimeoutdans les tests - Utilisez
vi.useFakeTimers()pour les composants avec timers - Lancez les tests en parallèle (comportement par défaut de Vitest)
Mock qui ne fonctionne pas
Assurez-vous que vi.mock() est appelé au niveau du module (pas dans un describe ou it) :
// ✅ Correct : au niveau du module
vi.mock("next/navigation", () => ({
useRouter: () => ({ push: vi.fn() }),
}));
// ❌ Incorrect : dans un bloc de test
describe("MyComponent", () => {
vi.mock("next/navigation"); // Trop tard !
});Prochaines étapes
Maintenant que vos tests unitaires sont en place :
- Explorez les tests E2E avec Playwright pour compléter votre stratégie de test — consultez notre tutoriel Playwright
- Ajoutez les tests de snapshot visuels avec Chromatic ou Percy
- Configurez la mutation testing avec Stryker pour valider la qualité de vos tests
- Explorez le Component Testing de Vitest Browser Mode pour tester dans un vrai navigateur
Conclusion
Vous avez maintenant un environnement de test complet pour votre application Next.js 15 :
- Vitest configuré avec React Testing Library pour des tests rapides et fiables
- Des tests de composants, hooks, formulaires et API routes
- Le mocking maîtrisé pour les modules, fetch et les providers
- La couverture de code mesurée avec des seuils configurables
- Un pipeline CI/CD prêt pour la production
Les tests ne sont pas une corvée — ce sont votre filet de sécurité. Chaque test que vous écrivez est une spécification vivante de votre application, un gardien contre les régressions, et une documentation qui ne ment jamais. Investissez dans vos tests aujourd'hui, et votre futur vous remerciera.
Discutez de votre projet avec nous
Nous sommes ici pour vous aider avec vos besoins en développement Web. Planifiez un appel pour discuter de votre projet et comment nous pouvons vous aider.
Trouvons les meilleures solutions pour vos besoins.
Articles connexes

Construire un Agent IA Autonome avec Agentic RAG et Next.js
Apprenez a construire un agent IA qui decide de maniere autonome quand et comment recuperer des informations depuis des bases de donnees vectorielles. Un guide pratique complet avec Vercel AI SDK et Next.js, accompagne d'exemples executables.

Better Auth avec Next.js 15 : Le Guide Complet d'Authentification pour 2026
Apprenez à implémenter une authentification complète dans Next.js 15 avec Better Auth. Ce tutoriel couvre email/mot de passe, OAuth, sessions, protection des routes et contrôle d'accès basé sur les rôles.

Construire une Application Full-Stack avec Drizzle ORM et Next.js 15 : Base de Donnees Type-Safe du Zero a la Production
Apprenez a construire une application full-stack type-safe avec Drizzle ORM et Next.js 15. Ce tutoriel pratique couvre la conception de schemas, les migrations, les Server Actions, les operations CRUD et le deploiement avec PostgreSQL.