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
Crea un log de presupuesto para Claude Code antes de que el coste se vuelva borroso
Registra quién usa Claude Code, para qué trabajo y qué resultado produjo en el equipo.
Revisión de 3 minutos antes del commit: confirma qué tocó Claude Code
Cómo detectar en 3 minutos los cambios que Claude Code amplió por su cuenta antes del commit: alcance, diff, prueba y stage selectivo.
El registro de riesgos antes de llevar Claude Code a tu equipo
Cómo armar un registro de riesgos que evita accidentes de permisos, CI y publicación al llevar Claude Code a tu equipo.