Advanced (Mis à jour: 22/07/2026)

Guide avancé Vitest avec Claude Code

Concevoir avec Claude Code des tests Vitest: doublures, faux minuteurs, jsdom, couverture, instantanés et CI.

Guide avancé Vitest avec Claude Code

Le problème que ce flux Vitest règle

Demander simplement à Claude Code “ajoute des tests Vitest” produit souvent des tests qui passent en local mais cassent autour du temps, du DOM, des API externes ou de la CI. Ce guide transforme ces zones risquées en un flux pratique: doublures de test pour remplacer les dépendances, faux minuteurs pour contrôler l’horloge, couverture pour repérer les branches non testées, jsdom pour la structure DOM, instantanés courts pour les contrats de rendu, et commandes CI qui se terminent vraiment.

Le 22 juillet 2026, cet article a été vérifié avec le guide CLI Vitest, la configuration watch et le guide de migration Vitest 4. Vitest 4 exige Vite 6 ou plus récent et Node 20 ou plus récent. En CI ou dans un terminal non interactif, vitest passe automatiquement en exécution unique; vitest run explicite néanmoins clairement l’intention du script.

Avec Claude Code, donnez le périmètre, la frontière à simuler, l’horloge à fixer ou non, l’environnement jsdom éventuel, et la commande de preuve. Pour compléter, lisez aussi stratégies de test avec Claude Code, guide MSW pour les API et tests E2E Playwright.

flowchart TD
  A["Spécification: succès et échecs"] --> B["Vitest config: node/jsdom/coverage"]
  B --> C["Unitaire: logique pure et frontières API"]
  B --> D["Temps: faux minuteurs et Date fixe"]
  B --> E["DOM: jsdom et instantanés"]
  C --> F["CI: vitest run --coverage"]
  D --> F
  E --> F

Commencer par une configuration stable

Installez Vitest, le fournisseur de couverture V8, jsdom et TypeScript. Une application Vite peut partager sa configuration, mais un vitest.config.ts dédié rend l’intention plus lisible pour Claude Code et pour les revues.

npm install -D vitest @vitest/coverage-v8 jsdom typescript
{
  "scripts": {
    "test": "vitest",
    "test:run": "vitest run",
    "coverage": "vitest run --coverage"
  }
}
// vitest.config.ts
import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    environment: "node",
    globals: false,
    restoreMocks: true,
    coverage: {
      provider: "v8",
      reporter: ["text", "html"],
      include: ["src/**/*.{ts,tsx}"],
      exclude: ["src/**/*.d.ts", "src/**/*.test.{ts,tsx}", "src/test/**"],
      thresholds: {
        lines: 80,
        functions: 80,
        branches: 75,
        statements: 80,
      },
    },
  },
});

globals: false force les imports explicites de describe et expect, ce qui réduit les surprises quand Claude Code déplace un test. restoreMocks: true aide, mais ne réinitialise ni les faux minuteurs ni le DOM.

Cas 1: Simuler une frontière API

Un test unitaire ne doit pas appeler une vraie API de commande, paiement ou utilisateur. Testez le contrat que vous maîtrisez: chemin, corps, validation d’entrée et traduction d’erreur.

// src/orders.ts
export type ApiClient = {
  post<T>(path: string, body: unknown): Promise<T>;
};

export class OrderError extends Error {
  constructor(message = "Order request failed") {
    super(message);
    this.name = "OrderError";
  }
}

type OrderInput = {
  sku: string;
  quantity: number;
};

type OrderResponse = {
  id: string;
  status: "accepted" | "queued";
};

export async function createOrder(api: ApiClient, input: OrderInput) {
  if (input.quantity < 1) {
    throw new OrderError("Quantity must be at least 1");
  }

  try {
    return await api.post<OrderResponse>("/orders", input);
  } catch {
    throw new OrderError("Order API failed");
  }
}
// src/orders.test.ts
import { describe, expect, it, vi } from "vitest";
import { createOrder, type ApiClient, OrderError } from "./orders";

describe("createOrder", () => {
  it("posts the order payload to the API", async () => {
    const api: ApiClient = {
      post: vi.fn().mockResolvedValue({ id: "ord_1", status: "accepted" }),
    };

    await expect(createOrder(api, { sku: "book-1", quantity: 2 })).resolves.toEqual({
      id: "ord_1",
      status: "accepted",
    });
    expect(api.post).toHaveBeenCalledWith("/orders", { sku: "book-1", quantity: 2 });
  });

  it("rejects invalid quantity before calling the API", async () => {
    const api: ApiClient = { post: vi.fn() };

    await expect(createOrder(api, { sku: "book-1", quantity: 0 })).rejects.toBeInstanceOf(
      OrderError,
    );
    expect(api.post).not.toHaveBeenCalled();
  });

  it("wraps transport errors in a domain error", async () => {
    const api: ApiClient = {
      post: vi.fn().mockRejectedValue(new Error("ECONNRESET")),
    };

    await expect(createOrder(api, { sku: "book-1", quantity: 1 })).rejects.toThrow(
      "Order API failed",
    );
  });
});

Cette injection de dépendance est souvent plus claire qu’un remplacement de module complet. vi.mock() reste utile, mais Vitest le hisse avant les imports; un mauvais ordre d’initialisation devient vite difficile à lire.

Cas 2: Fixer le temps avec de faux minuteurs

Essais gratuits, relances, notifications et anti-rebond deviennent instables si le test attend le temps réel. Vitest permet de contrôler setTimeout, setInterval et la date système.

// src/trial.ts
const DAY_MS = 24 * 60 * 60 * 1000;

export function getTrialEndsAt(days = 7) {
  return new Date(Date.now() + days * DAY_MS).toISOString();
}

export function scheduleTrialReminder(send: () => void, days = 7) {
  return setTimeout(send, days * DAY_MS);
}
// src/trial.test.ts
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { getTrialEndsAt, scheduleTrialReminder } from "./trial";

describe("trial reminder", () => {
  beforeEach(() => {
    vi.useFakeTimers();
    vi.setSystemTime(new Date("2026-06-03T00:00:00.000Z"));
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  it("calculates the trial end date from the fixed clock", () => {
    expect(getTrialEndsAt()).toBe("2026-06-10T00:00:00.000Z");
  });

  it("runs the reminder after the configured number of days", () => {
    const send = vi.fn();
    const timer = scheduleTrialReminder(send, 3);

    vi.advanceTimersByTime(3 * 24 * 60 * 60 * 1000 - 1);
    expect(send).not.toHaveBeenCalled();

    vi.advanceTimersByTime(1);
    expect(send).toHaveBeenCalledTimes(1);
    clearTimeout(timer);
  });
});

Le piège classique est d’oublier vi.useRealTimers(). Une horloge fausse laissée par un fichier peut faire échouer un autre test. Quand une promesse est impliquée, utilisez aussi await. Les limites de date et fuseau horaire sont détaillées dans gestion des dates avec Claude Code.

Cas 3: Protéger le DOM avec jsdom et instantanés

jsdom imite les API DOM dans Node. Il convient à la structure, au texte et aux attributs d’accessibilité. Il ne remplace pas un vrai navigateur pour la mise en page, le focus réel, Canvas ou les régressions visuelles.

// src/notice.ts
export function renderNotice(target: HTMLElement, message: string) {
  target.innerHTML = "";

  const notice = document.createElement("p");
  notice.setAttribute("role", "status");
  notice.dataset.testid = "notice";
  notice.textContent = message;

  target.append(notice);
  return notice;
}
// src/notice.test.ts
// @vitest-environment jsdom
import { afterEach, describe, expect, it } from "vitest";
import { renderNotice } from "./notice";

afterEach(() => {
  document.body.innerHTML = "";
});

describe("renderNotice", () => {
  it("renders an accessible status message", () => {
    document.body.innerHTML = '<div id="app"></div>';
    const target = document.querySelector<HTMLDivElement>("#app");
    if (!target) throw new Error("missing #app");

    const notice = renderNotice(target, "Enregistré");

    expect(notice.getAttribute("role")).toBe("status");
    expect(notice.textContent).toBe("Enregistré");
    expect({
      html: document.body.innerHTML,
      text: notice.textContent,
    }).toMatchInlineSnapshot(`
      {
        "html": "<div id=\\"app\\"><p role=\\"status\\" data-testid=\\"notice\\">Enregistré</p></div>",
        "text": "Enregistré",
      }
    `);
  });
});

Un instantané doit rester court. Vérifiez les attributs importants avec expect, puis gardez une petite structure en instantané. Pour l’interaction réelle et le rendu visuel, utilisez Playwright.

Couverture et CI

La couverture sert à trouver les branches non vérifiées, pas à fabriquer un joli pourcentage. Vitest documente les fournisseurs V8 et Istanbul, avec V8 comme fournisseur par défaut. Ajoutez coverage.include, sinon un fichier jamais importé par les tests peut disparaître du rapport.

# .github/workflows/vitest.yml
name: vitest

on:
  pull_request:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npm run test:run
      - run: npm run coverage

En CI, utilisez explicitement vitest run. vitest ne surveille que lorsque CI est faux et que le terminal est interactif; sinon il s’exécute une fois. Pour un outil interactif comme lint-staged, utilisez vitest related --run. Le flux complet est traité dans configuration CI/CD avec Claude Code.

Prompt pratique pour Claude Code

Ajoute des tests Vitest pour src/orders.ts.
Teste seulement createOrder.
Simule l'API externe avec vi.fn(); ne fais aucun appel HTTP réel.
Inclue les cas succès, entrée invalide et échec de transport.
N'utilise pas de faux minuteurs ni jsdom sauf si le code l'exige.
Après modification, indique la commande attendue npm run test:run et les risques restants.

Ce prompt fournit le périmètre, la frontière simulée, les échecs obligatoires et la preuve attendue. Ajoutez la même règle aux bonnes pratiques CLAUDE.md.

Trois cas d’usage

Cas d’usage 1: un formulaire de commande ou d’inscription. Un mauvais chemin ou corps de requête peut bloquer une conversion. Claude Code peut rédiger les tests de frontière avec vi.fn() pour succès, entrée invalide et échec de transport; une personne doit approuver paiement, facturation, stock et règles de données client.

Cas d’usage 2: rappels d’essai, de rendez-vous ou de renouvellement. Claude Code peut fixer Date, avancer les faux minuteurs et prouver que le callback part une seule fois. Une personne doit approuver fuseaux horaires, jours ouvrés, envoi réel d’email ou SMS et impacts de facturation.

Cas d’usage 3: petits contrats DOM comme message enregistré, validation ou bannière d’administration. Claude Code peut vérifier texte, rôles et petit instantané avec jsdom. Une personne doit approuver comportement réel du navigateur, focus, régression visuelle et texte visible par les clients.

Ce que Claude Code fait et ce que l’humain approuve

ZoneÀ déléguer à Claude CodeApprobation humaine requise
TestsDoublures, faux minuteurs, assertions jsdom, commandes de couvertureVérifier que les assertions ne réduisent pas le comportement
CIProposer vitest run et vitest run --coverageActiver checks obligatoires, runners coûteux ou déploiement
DonnéesUtiliser des identifiants synthétiques comme ord_1 et book-1Contrôler données client, secrets, API keys et paiements
NavigateurGarder les contrats DOM dans VitestValider les parcours réels avec Playwright ou revue manuelle

Pièges et corrections

ÉchecSymptômeCorrection
Doublures non restauréesAppels ou fausses implémentations fuientUtiliser restoreMocks, vi.clearAllMocks() ou vi.restoreAllMocks() selon le besoin
Faux minuteurs non restaurésUn autre fichier échoue de façon aléatoireAppeler vi.useRealTimers() dans afterEach
jsdom pris pour un navigateur réelCSS, mise en page, images ou Canvas diffèrentVitest pour le contrat DOM, Playwright pour le navigateur
Instantané trop largeLa revue devient bruyanteNe sauvegarder que de petites structures
coverage.include absentDes fichiers non testés restent invisiblesInclure explicitement src/**/*.{ts,tsx}
Asynchrone non attenduFaux positifsUtiliser await expect(promise).resolves ou rejects
Un runner interactif ouvre la surveillanceLe processus attend des changementsUtiliser vitest run; pour les fichiers modifiés, vitest related --run

La correction ne consiste pas à rendre la suite verte à n’importe quel prix. Quand un test échoue, collez l’erreur complète, interdisez à Claude Code de supprimer des assertions et demandez d’abord une correction de l’implémentation ou du mock. Sécurité, production, facturation et données client restent soumis à une revue humaine du diff.

Pour appliquer ces règles dans votre dépôt, utilisez les modèles pratiques de ClaudeCodeLab pour les standards de test, les prompts de revue et les portes CI.

Résultat vérifié

Le 22 juillet 2026, les exemples TypeScript ont été extraits dans un projet temporaire puis exécutés avec Node v24.14.1 et Vitest 4.1.10. vitest run et vitest run --coverage ont validé 4 fichiers et 7 tests couvrant la frontière API, le mock de module, l’horloge fixe, la limite du minuteur, les attributs DOM et l’instantané inline. La couverture était de 94,73% statements, 100% branches, 85,71% functions et 94,73% lines. Le mailer était simulé; aucun e-mail réel ni API de production n’a été appelé.

#Claude Code #Vitest #tests #TypeScript #assurance qualité
Gratuit

PDF gratuit: cheatsheet Claude Code

Saisissez votre email et téléchargez une page avec commandes, habitudes de review et workflow sûr.

Nous protégeons vos données et n'envoyons pas de spam.

Masa

À propos de l'auteur

Masa

Ingénieur spécialisé dans les workflows pratiques avec Claude Code.