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

AI Bot
Par AI Bot ·

Chargement du lecteur de synthèse vocale...

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èreJestVitest
VitesseLent (transformation CJS)Ultra-rapide (ESM natif via Vite)
ConfigurationComplexe avec Next.jsMinimale avec next/vitest
ESMSupport partielSupport natif complet
Hot reloadNonMode watch intelligent
APIPropriétaireCompatible Jest (migration facile)
TypeScriptNécessite ts-jestSupport 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-app

Vé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 jsdom

Voici ce que fait chaque paquet :

PaquetRôle
vitestFramework de tests
@vitejs/plugin-reactSupport JSX/TSX dans Vitest
@testing-library/reactUtilitaires pour tester les composants React
@testing-library/jest-domMatchers DOM personnalisés (toBeInTheDocument, etc.)
@testing-library/user-eventSimulation réaliste des interactions utilisateur
jsdomEnvironnement 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 test

Vous 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:coverage

Vitest 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=verbose pour identifier les tests lents
  • Évitez les setTimeout dans 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.


Vous voulez lire plus de tutoriels? Découvrez notre dernier tutoriel sur Détection et identification des maladies des feuilles de plantes avec YOLOv4.

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.

30 min read·