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 22 de julio de 2026, este artículo se contrastó con la guía CLI de Vitest, la configuración de watch y la guía de migración a Vitest 4. Vitest 4 requiere Vite 6 o posterior y Node 20 o posterior. En CI o en una terminal no interactiva, vitest cambia automáticamente a ejecución única; vitest run sigue dejando clara la intención del script.
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 de forma explícita. vitest solo vigila cuando CI es falso y la terminal es interactiva; en CI o sin interacción ejecuta una vez. Para herramientas interactivas como lint-staged, usa vitest related --run. 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.
Tres casos de uso
Caso de uso 1: un formulario de pedido o alta. Una ruta o cuerpo incorrecto puede bloquear una compra. Claude Code puede redactar las pruebas de frontera con vi.fn() para éxito, entrada inválida y fallo de transporte; una persona debe aprobar pagos, facturación, inventario y datos de clientes.
Caso de uso 2: recordatorios de prueba, cita o renovación. Claude Code puede congelar Date, avanzar temporizadores falsos y demostrar que el callback se ejecuta una sola vez. Una persona debe aprobar zona horaria, días hábiles, envío real de email o SMS y cualquier efecto de facturación.
Caso de uso 3: contratos DOM pequeños como aviso guardado, mensaje de validación o banner administrativo. Claude Code puede comprobar texto, roles y una instantánea pequeña con jsdom. Una persona debe aprobar comportamiento en navegador real, foco, regresión visual y textos visibles para usuarios.
Qué delegar en Claude Code y qué aprobar
| Área | Delegable en Claude Code | Requiere aprobación humana |
|---|---|---|
| Código de prueba | Dobles, temporizadores falsos, aserciones jsdom y comandos de cobertura | Confirmar que las aserciones no debilitan el comportamiento |
| CI | Proponer vitest run y vitest run --coverage | Activar checks obligatorios, runners con coste o despliegue |
| Datos | Usar IDs sintéticos como ord_1 y book-1 | Revisar datos de clientes, secretos, API keys e IDs de pago |
| Navegador | Mantener contratos DOM en Vitest | Validar flujos reales con Playwright o revisión manual |
Errores frecuentes y correcciones
| 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 |
| Un runner interactivo entra en vigilancia | El proceso espera cambios | Usar vitest run; para cambios, vitest related --run |
La corrección no consiste en “ponerlo verde” a cualquier precio. Cuando una prueba falla, pega el error completo, pide a Claude Code que no borre aserciones y exige que primero corrija implementación o configuración del mock. Seguridad, producción, facturación y datos de clientes quedan fuera de la edición automática hasta que una persona revise el diff.
Para aplicar estas reglas de revisión en tu repositorio, usa las plantillas prácticas de ClaudeCodeLab para estándares de pruebas, prompts de revisión y puertas de CI.
Resultado práctico
El 22 de julio de 2026, los ejemplos TypeScript se extrajeron a un proyecto temporal y se ejecutaron con Node v24.14.1 y Vitest 4.1.10. vitest run y vitest run --coverage aprobaron 4 archivos y 7 pruebas de frontera API, mock de módulo, reloj fijo, límite de temporizador, atributos DOM e instantánea inline. La cobertura fue 94,73% statements, 100% branches, 85,71% functions y 94,73% lines. El mailer se simuló; no se enviaron correos ni se llamó a API de producción.
Artículos relacionados
Pruebas E2E con Claude Code y Playwright en producción
Diseña E2E con Claude Code y Playwright: móvil, auth state, Trace Viewer, selectores y retries en CI.
ESLint Flat Config con Claude Code para proyectos reales
Configura ESLint Flat Config con Claude Code para TypeScript, React, Astro, CI y prompts de corrección.
Tipos utilitarios de TypeScript con Claude Code: guía práctica
Aprende Pick, Omit, Partial, Record, ReturnType y Awaited con ejemplos ejecutables para Claude Code.
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.