Advanced (更新: 2026/6/3)

Claude Code 与 Vitest 进阶测试实战指南

用Claude Code设计Vitest的模拟、计时、jsdom、覆盖率、快照与CI。

Claude Code 与 Vitest 进阶测试实战指南

这套 Vitest 流程解决什么问题

只让 Claude Code “写几个 Vitest 测试”通常不够。代码可能在本地通过,却在时间、DOM、外部 API 或 CI 中暴露问题。本文把这些风险整理成一套可复用的流程:用模拟对象替代外部依赖,用伪计时器固定时间,用覆盖率发现未测试分支,用 jsdom 检查 DOM 结构,用快照保护小范围渲染契约,并用 CI 命令稳定退出。

本文在 2026 年 6 月 3 日查看了 Vitest 官方文档:Getting StartedMockingTimersDatesTest EnvironmentCoverageSnapshotCLI。官方文档说明 Vitest 4 系列需要较新的 Node 环境,并区分默认监视模式与 vitest run。CI 中一定要使用一次性运行命令。

使用 Claude Code 时,不要只要求“补测试”。请说明哪个边界要模拟、时钟是否固定、jsdom 是否足够、失败后运行什么命令验证。相关背景可继续阅读 Claude Code 测试策略MSW API 模拟指南Playwright E2E 测试

flowchart TD
  A["规格: 成功与失败条件"] --> B["Vitest config: node/jsdom/coverage"]
  B --> C["单元测试: 纯逻辑与API边界"]
  B --> D["时间测试: 伪计时器与固定Date"]
  B --> E["DOM测试: jsdom与快照"]
  C --> F["CI: vitest run --coverage"]
  D --> F
  E --> F

先固定最小配置

先安装 Vitest、V8 覆盖率提供器、jsdom 和 TypeScript。已有 Vite 应用可以共用配置,但单独放一个 vitest.config.ts 更清晰,Claude Code 也更容易按意图修改。

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: falsedescribeexpect 的来源一眼可见,适合多人维护和 AI 生成代码。restoreMocks: true 可以减少模拟实现泄漏,但不会自动恢复伪计时器,也不会清空 DOM,所以后面的测试仍然需要 afterEach

用例1: 模拟 API 边界

单元测试不应该真的调用订单、支付或用户 API。更稳妥的做法是验证自己负责的契约:请求路径、请求体、输入校验和错误转换。

// 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",
    );
  });
});

这种依赖注入写法通常比整模块模拟更容易读。vi.mock() 仍然有用,但 Vitest 会把它提升到 import 之前,初学者和 AI 生成代码很容易把初始化顺序写错。能用小的 vi.fn() 证明行为时,就不要把整个模块换掉。

用例2: 用伪计时器固定期限逻辑

试用期、重试、通知和防抖逻辑如果等待真实时间,测试会慢而且不稳定。Vitest 的伪计时器可以控制 setTimeoutsetInterval 和系统日期。

// 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);
  });
});

最常见的失败是忘记 vi.useRealTimers()。上一个文件留下的假时钟会让下一个文件偶发失败。涉及 Promise 时也必须 await,否则本该失败的异步分支可能看起来通过了。日期和时区边界可参考 Claude Code 日期时间处理指南

用例3: 用 jsdom 和快照保护显示契约

jsdom 是在 Node 中模拟浏览器 DOM API 的环境。它适合检查 DOM 结构、文本和无障碍属性,但不能完整替代真实浏览器的布局、焦点、Canvas 和视觉回归测试。

// 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, "已保存");

    expect(notice.getAttribute("role")).toBe("status");
    expect(notice.textContent).toBe("已保存");
    expect({
      html: document.body.innerHTML,
      text: notice.textContent,
    }).toMatchInlineSnapshot(`
      {
        "html": "<div id=\\"app\\"><p role=\\"status\\" data-testid=\\"notice\\">已保存</p></div>",
        "text": "已保存",
      }
    `);
  });
});

快照适合小范围结构,不适合整页 HTML。关键属性应直接用 expect 断言,快照只保存不应该随意漂移的紧凑结构。真实浏览器交互请交给 Playwright。

把覆盖率和 CI 变成质量门

覆盖率不是为了追高数字,而是为了发现未测试分支。Vitest 文档说明可使用 V8 和 Istanbul 覆盖率提供器,默认提供器是 V8。请显式配置 coverage.include,否则未被测试 import 的新文件可能不会出现在报告里。

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

CI 中请使用 vitest run。只写 vitest 在某些环境会进入监视模式,导致任务不退出。更完整的流水线设计可阅读 Claude Code CI/CD 设置指南

给 Claude Code 的提示词

请为 src/orders.ts 添加 Vitest 测试。
只测试 createOrder。
外部 API 用 vi.fn() 模拟,不要发起真实 HTTP 请求。
必须包含成功、输入不合法、传输失败三个用例。
除非代码需要,否则不要使用伪计时器或 jsdom。
编辑后说明预期运行 npm run test:run,并列出剩余风险。

这段提示词同时给出了范围、模拟边界、失败场景和验证命令。团队使用时,可以把同样规则写入 CLAUDE.md 最佳实践

常见失败模式

失败模式症状修复
模拟没有恢复调用次数或假实现泄漏到下一个测试目的明确地使用 restoreMocksvi.clearAllMocks()vi.restoreAllMocks()
伪计时器没有恢复另一个文件的时间测试偶发失败afterEach 中调用 vi.useRealTimers()
把 jsdom 当真实浏览器CSS、布局、图像或 Canvas 与生产环境不同Vitest 检查 DOM 契约,Playwright 检查浏览器行为
快照太大评审差异充满噪音只快照小结构,关键属性直接断言
缺少 coverage.include未测试文件不可见显式包含 src/**/*.{ts,tsx}
异步没有等待假阳性通过使用 await expect(promise).resolvesrejects
CI 使用监视模式任务不结束使用 vitest runvitest related --run

如果要把这套流程落到自己的仓库,ClaudeCodeLab 提供 Claude Code 培训实用模板,可用于团队测试标准、评审提示词和 CI 质量门。

实际验证结果

最终得到的是三组可以复制的 Vitest 用例:API 边界测试、固定时间测试、jsdom 渲染测试与小快照。我在发布前检查了 Vitest 官方文档、内部链接、外部链接、代码围栏、updatedDate、覆盖率设置,以及 CI 中使用 vitest run 的要求。

#Claude Code #Vitest #测试 #TypeScript #质量保证
免费

免费 PDF: Claude Code 速查表

输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。

我们会妥善保护你的信息,不发送垃圾邮件。

把 Claude Code 变成真正能带来结果的工作流

先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。

Masa

关于作者

Masa

专注 Claude Code 实务流程、团队导入和内容转化的工程师。