Guía avanzada de Vitest con Claude Code
Diseña pruebas Vitest con Claude Code: dobles, temporizadores falsos, jsdom, cobertura, instantáneas y CI.
Qué resuelve este flujo de Vitest
Pedirle a Claude Code “agrega pruebas con Vitest” no basta. Las pruebas pueden pasar en local y fallar alrededor del tiempo, el DOM, las API externas o CI. Este artículo convierte esas zonas de riesgo en un flujo práctico: dobles de prueba para reemplazar dependencias, temporizadores falsos para controlar el reloj, cobertura para descubrir ramas sin verificar, jsdom para estructura de DOM, instantáneas pequeñas para contratos de renderizado y comandos de CI que terminan correctamente.
El 3 de junio de 2026 revisé la documentación oficial de Vitest: Getting Started, Mocking, Timers, Dates, Test Environment, Coverage, Snapshot y CLI. La documentación de Vitest 4 distingue el modo de vigilancia de vitest run; esa diferencia es crítica en CI.
Usa Claude Code como compañero de diseño de pruebas. Dile qué frontera debe simularse, si el reloj debe fijarse, si jsdom es suficiente y qué comando demostrará el resultado. Para ampliar el contexto, lee estrategias de pruebas con Claude Code, guía de MSW para API y pruebas E2E con Playwright.
flowchart TD
A["Especificación: éxito y fallos"] --> B["Vitest config: node/jsdom/coverage"]
B --> C["Unidad: lógica pura y fronteras API"]
B --> D["Tiempo: temporizadores falsos y Date fijo"]
B --> E["DOM: jsdom e instantáneas"]
C --> F["CI: vitest run --coverage"]
D --> F
E --> F
Empieza con una configuración estable
Instala Vitest, el proveedor de cobertura V8, jsdom y TypeScript. En una aplicación Vite puedes compartir configuración, pero un vitest.config.ts separado deja clara la intención para Claude Code y para revisión humana.
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 obliga a importar describe y expect, lo que reduce ambigüedad cuando Claude Code mueve pruebas entre archivos. restoreMocks: true ayuda, pero no limpia temporizadores falsos ni el DOM; eso se hace explícitamente.
Caso 1: Simular una frontera de API
Una prueba unitaria no debería llamar una API real de pedidos, pagos o usuarios. Verifica el contrato que controlas: ruta, cuerpo, validación de entrada y conversión de errores.
// 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",
);
});
});
Este estilo con inyección de dependencia suele ser más legible que reemplazar un módulo completo. vi.mock() sirve, pero Vitest lo eleva antes de los imports, y el orden de inicialización puede confundir a principiantes y a código generado por IA.
Caso 2: Fijar el tiempo con temporizadores falsos
Pruebas de periodos de prueba, reintentos, notificaciones y antirrebote se vuelven inestables si esperan tiempo real. Vitest permite controlar setTimeout, setInterval y la fecha del sistema.
// 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);
});
});
El fallo típico es olvidar vi.useRealTimers(). Si el reloj falso queda vivo, otra prueba puede fallar de forma intermitente. Si hay promesas, usa await. Para límites de fecha y zona horaria, consulta manejo de fechas con Claude Code.
Caso 3: Proteger DOM con jsdom e instantáneas
jsdom imita API de DOM dentro de Node. Sirve para estructura, texto y atributos de accesibilidad, pero no reemplaza un navegador real para layout, foco, Canvas o regresión visual.
// 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, "Guardado");
expect(notice.getAttribute("role")).toBe("status");
expect(notice.textContent).toBe("Guardado");
expect({
html: document.body.innerHTML,
text: notice.textContent,
}).toMatchInlineSnapshot(`
{
"html": "<div id=\\"app\\"><p role=\\"status\\" data-testid=\\"notice\\">Guardado</p></div>",
"text": "Guardado",
}
`);
});
});
Las instantáneas deben ser pequeñas. Para el estado esencial usa expect directo; reserva la instantánea para una estructura compacta que no debería cambiar sin revisión.
Cobertura y CI
La cobertura debe mostrar ramas no verificadas, no inflar números. Vitest documenta proveedores V8 e Istanbul, con V8 como proveedor predeterminado. Declara coverage.include; si no, archivos nuevos que nadie importa pueden quedar fuera del informe.
# .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 usa vitest run, no solo vitest, porque el modo de vigilancia puede dejar el trabajo abierto. Para la tubería completa, revisa CI/CD con Claude Code.
Prompt útil para Claude Code
Agrega pruebas Vitest para src/orders.ts.
Prueba solo createOrder.
Simula la API externa con vi.fn(); no hagas llamadas HTTP reales.
Incluye éxito, entrada inválida y fallo de transporte.
No uses temporizadores falsos ni jsdom salvo que el código lo requiera.
Después de editar, informa el comando esperado npm run test:run y los riesgos restantes.
Este prompt define alcance, frontera simulada, fallos obligatorios y prueba de verificación. Añade reglas parecidas a buenas prácticas de CLAUDE.md para que las sesiones futuras no terminen en una sola prueba feliz.
Fallos comunes
| Fallo | Síntoma | Solución |
|---|---|---|
| No restaurar dobles | Conteos o implementaciones falsas se filtran | Usar restoreMocks, vi.clearAllMocks() o vi.restoreAllMocks() según el caso |
| No restaurar temporizadores | Pruebas de tiempo fallan en otro archivo | Llamar vi.useRealTimers() en afterEach |
| Tratar jsdom como navegador real | CSS, layout, imágenes o Canvas difieren | Vitest para contrato DOM, Playwright para navegador |
| Instantáneas enormes | Revisión con mucho ruido | Guardar solo estructuras pequeñas |
Falta coverage.include | Archivos sin pruebas no aparecen | Incluir src/**/*.{ts,tsx} explícitamente |
| No esperar asincronía | Falsos positivos | Usar await expect(promise).resolves o rejects |
| CI en modo vigilancia | El trabajo no termina | Usar vitest run o vitest related --run |
Si quieres adaptar este flujo a tu repositorio, ClaudeCodeLab ofrece formación en inglés y plantillas prácticas para estándares de pruebas, prompts de revisión y puertas de CI.
Resultado práctico
El resultado es una base Vitest con tres casos copiables: prueba de frontera API, prueba de tiempo fijo y prueba DOM con jsdom e instantánea pequeña. Antes de publicar, revisé documentación oficial, enlaces internos, enlaces externos, cercas de código, updatedDate, cobertura y el uso de vitest run en CI.
PDF gratis: cheatsheet de Claude Code
Introduce tu email y descarga una hoja con comandos, hábitos de revisión y flujos seguros.
Cuidamos tus datos y no enviamos spam.
Sobre el autor
Masa
Ingeniero enfocado en workflows prácticos con Claude Code.
Artículos relacionados
Permission receipt para Claude Code: alcance, prueba y rollback
Patrón de permission receipt para Claude Code: acciones permitidas, aprobación, pruebas, rollback y CTA de ingresos.
Agent Harness seguro para Claude Code y Codex: permisos, verificacion y rollback
Diseña un Agent Harness seguro para Claude Code y Codex con permisos, plan, verificaciones y rollback.
Subagentes de Claude Code: guía práctica para delegar trabajo de forma segura
Guía práctica de subagentes en Claude Code para dividir artículos y código: reglas, prompts, riesgos y checklist.