Advanced (Actualizado: 3/6/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 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

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
CI en modo vigilanciaEl trabajo no terminaUsar 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.

#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.