Advanced (Actualizado: 22/7/2026)

Guía avanzada de Vitest con Claude Code

Diseña pruebas Vitest con Claude Code: dobles, temporizadores falsos, jsdom, cobertura, instantáneas y CI.

Guía avanzada de Vitest con Claude Code

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

ÁreaDelegable en Claude CodeRequiere aprobación humana
Código de pruebaDobles, temporizadores falsos, aserciones jsdom y comandos de coberturaConfirmar que las aserciones no debilitan el comportamiento
CIProponer vitest run y vitest run --coverageActivar checks obligatorios, runners con coste o despliegue
DatosUsar IDs sintéticos como ord_1 y book-1Revisar datos de clientes, secretos, API keys e IDs de pago
NavegadorMantener contratos DOM en VitestValidar flujos reales con Playwright o revisión manual

Errores frecuentes y correcciones

FalloSíntomaSolución
No restaurar doblesConteos o implementaciones falsas se filtranUsar restoreMocks, vi.clearAllMocks() o vi.restoreAllMocks() según el caso
No restaurar temporizadoresPruebas de tiempo fallan en otro archivoLlamar vi.useRealTimers() en afterEach
Tratar jsdom como navegador realCSS, layout, imágenes o Canvas difierenVitest para contrato DOM, Playwright para navegador
Instantáneas enormesRevisión con mucho ruidoGuardar solo estructuras pequeñas
Falta coverage.includeArchivos sin pruebas no aparecenIncluir src/**/*.{ts,tsx} explícitamente
No esperar asincroníaFalsos positivosUsar await expect(promise).resolves o rejects
Un runner interactivo entra en vigilanciaEl proceso espera cambiosUsar 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.

#Claude Code #Vitest #pruebas #TypeScript #garantía de calidad
Gratis

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.

Masa

Sobre el autor

Masa

Ingeniero enfocado en workflows prácticos con Claude Code.