Claude Code 与 Vitest 进阶测试实战指南
用Claude Code设计Vitest的模拟、计时、jsdom、覆盖率、快照与CI。
这套 Vitest 流程解决什么问题
只让 Claude Code “写几个 Vitest 测试”通常不够。代码可能在本地通过,却在时间、DOM、外部 API 或 CI 中暴露问题。本文把这些风险整理成一套可复用的流程:用模拟对象替代外部依赖,用伪计时器固定时间,用覆盖率发现未测试分支,用 jsdom 检查 DOM 结构,用快照保护小范围渲染契约,并用 CI 命令稳定退出。
本文在 2026 年 6 月 3 日查看了 Vitest 官方文档:Getting Started、Mocking、Timers、Dates、Test Environment、Coverage、Snapshot 和 CLI。官方文档说明 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: false 让 describe、expect 的来源一眼可见,适合多人维护和 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 的伪计时器可以控制 setTimeout、setInterval 和系统日期。
// 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 最佳实践。
常见失败模式
| 失败模式 | 症状 | 修复 |
|---|---|---|
| 模拟没有恢复 | 调用次数或假实现泄漏到下一个测试 | 目的明确地使用 restoreMocks、vi.clearAllMocks() 或 vi.restoreAllMocks() |
| 伪计时器没有恢复 | 另一个文件的时间测试偶发失败 | 在 afterEach 中调用 vi.useRealTimers() |
| 把 jsdom 当真实浏览器 | CSS、布局、图像或 Canvas 与生产环境不同 | Vitest 检查 DOM 契约,Playwright 检查浏览器行为 |
| 快照太大 | 评审差异充满噪音 | 只快照小结构,关键属性直接断言 |
缺少 coverage.include | 未测试文件不可见 | 显式包含 src/**/*.{ts,tsx} |
| 异步没有等待 | 假阳性通过 | 使用 await expect(promise).resolves 或 rejects |
| CI 使用监视模式 | 任务不结束 | 使用 vitest run 或 vitest related --run |
如果要把这套流程落到自己的仓库,ClaudeCodeLab 提供 Claude Code 培训 与 实用模板,可用于团队测试标准、评审提示词和 CI 质量门。
实际验证结果
最终得到的是三组可以复制的 Vitest 用例:API 边界测试、固定时间测试、jsdom 渲染测试与小快照。我在发布前检查了 Vitest 官方文档、内部链接、外部链接、代码围栏、updatedDate、覆盖率设置,以及 CI 中使用 vitest run 的要求。
免费 PDF: Claude Code 速查表
输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。
我们会妥善保护你的信息,不发送垃圾邮件。
把 Claude Code 变成真正能带来结果的工作流
先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。
关于作者
Masa
专注 Claude Code 实务流程、团队导入和内容转化的工程师。
相关文章
Claude Code Permission Receipt Pattern:记录权限、证据和回滚方式
Claude Code 权限 receipt:记录允许动作、需要批准的边界、验证命令、回滚说明,以及 Gumroad 和咨询 CTA 检查。
Claude Code/Codex 安全 Agent Harness 实战:权限、验证与回滚
用权限策略、执行计划、验证脚本和回滚日志,为 Claude Code 与 Codex 搭建更安全的 AI Agent 工作流。
Claude Code 子代理实战指南:安全委派并行文章与代码工作
用 Claude Code 子代理安全拆分文章和代码工作:委派规则、提示词模板、失败模式与检查清单。