Guide avancé Vitest avec Claude Code
Concevoir avec Claude Code des tests Vitest: doublures, faux minuteurs, jsdom, couverture, instantanés et CI.
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 Code | Approbation humaine requise |
|---|---|---|
| Tests | Doublures, faux minuteurs, assertions jsdom, commandes de couverture | Vérifier que les assertions ne réduisent pas le comportement |
| CI | Proposer vitest run et vitest run --coverage | Activer checks obligatoires, runners coûteux ou déploiement |
| Données | Utiliser des identifiants synthétiques comme ord_1 et book-1 | Contrôler données client, secrets, API keys et paiements |
| Navigateur | Garder les contrats DOM dans Vitest | Valider les parcours réels avec Playwright ou revue manuelle |
Pièges et corrections
| Échec | Symptôme | Correction |
|---|---|---|
| Doublures non restaurées | Appels ou fausses implémentations fuient | Utiliser restoreMocks, vi.clearAllMocks() ou vi.restoreAllMocks() selon le besoin |
| Faux minuteurs non restaurés | Un autre fichier échoue de façon aléatoire | Appeler vi.useRealTimers() dans afterEach |
| jsdom pris pour un navigateur réel | CSS, mise en page, images ou Canvas diffèrent | Vitest pour le contrat DOM, Playwright pour le navigateur |
| Instantané trop large | La revue devient bruyante | Ne sauvegarder que de petites structures |
coverage.include absent | Des fichiers non testés restent invisibles | Inclure explicitement src/**/*.{ts,tsx} |
| Asynchrone non attendu | Faux positifs | Utiliser await expect(promise).resolves ou rejects |
| Un runner interactif ouvre la surveillance | Le processus attend des changements | Utiliser 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é.
Articles liés
Tests E2E avec Claude Code et Playwright en production
Concevez des E2E Playwright avec Claude Code: mobile, auth state, Trace Viewer, sélecteurs et retries CI.
ESLint Flat Config avec Claude Code pour un vrai projet
Configurez ESLint Flat Config avec Claude Code pour TypeScript, React, Astro, CI et prompts de correction.
Types utilitaires TypeScript avec Claude Code : guide pratique
Apprenez Pick, Omit, Partial, Record, ReturnType et Awaited avec des exemples exécutables pour Claude Code.
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.
À propos de l'auteur
Masa
Ingénieur spécialisé dans les workflows pratiques avec Claude Code.