Advanced (Mis à jour: 03/06/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 3 juin 2026, j’ai vérifié la documentation officielle Vitest: Getting Started, Mocking, Timers, Dates, Test Environment, Coverage, Snapshot et CLI. La documentation de Vitest 4 distingue le mode surveillance de vitest run; cette différence compte beaucoup en CI.

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 vitest run. La commande vitest seule peut ouvrir le mode surveillance. 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.

Échecs fréquents

É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
CI en mode surveillanceLe travail ne se termine pasUtiliser vitest run ou vitest related --run

Pour adapter ce flux à votre dépôt, ClaudeCodeLab propose une page de formation en anglais et des modèles pratiques pour les standards de test, les prompts de revue et les portes CI.

Résultat vérifié

Le résultat est une base Vitest avec trois cas copiables: frontière API, temps fixé, et rendu jsdom avec petit instantané. Avant publication, j’ai vérifié les documents officiels Vitest, les liens internes, les liens externes, les blocs de code, updatedDate, la couverture et l’usage de vitest run en CI.

#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.