Claude Code 与 Vitest 进阶测试实战指南
用Claude Code设计Vitest的模拟、计时、jsdom、覆盖率、快照与CI。
这套 Vitest 流程解决什么问题
只让 Claude Code “写几个 Vitest 测试”通常不够。代码可能在本地通过,却在时间、DOM、外部 API 或 CI 中暴露问题。本文把这些风险整理成一套可复用的流程:用模拟对象替代外部依赖,用伪计时器固定时间,用覆盖率发现未测试分支,用 jsdom 检查 DOM 结构,用快照保护小范围渲染契约,并用 CI 命令稳定退出。
本文于 2026 年 7 月 22 日对照了官方 Vitest CLI 指南、watch 配置和 Vitest 4 迁移指南。Vitest 4 要求 Vite 6 或更高版本以及 Node 20 或更高版本。在 CI 或非交互终端中,vitest 会自动切换为单次运行;显式写成 vitest run 仍能让脚本意图更清楚。
使用 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 只在 CI 为 false 且终端可交互时进入监视模式;在 CI 或非交互终端中只运行一次。lint-staged 等交互工具可使用 vitest related --run。更完整的流水线设计可阅读 Claude Code CI/CD 设置指南。
给 Claude Code 的提示词
请为 src/orders.ts 添加 Vitest 测试。
只测试 createOrder。
外部 API 用 vi.fn() 模拟,不要发起真实 HTTP 请求。
必须包含成功、输入不合法、传输失败三个用例。
除非代码需要,否则不要使用伪计时器或 jsdom。
编辑后说明预期运行 npm run test:run,并列出剩余风险。
这段提示词同时给出了范围、模拟边界、失败场景和验证命令。团队使用时,可以把同样规则写入 CLAUDE.md 最佳实践。
三个使用场景
使用场景一是订单或注册表单。错误的请求体可能让用户无法完成购买或开通。Claude Code 可以编写 vi.fn() API 边界测试,覆盖成功、输入不合法和传输失败;人必须审核支付、计费、库存和客户数据规则。
使用场景二是试用、预约或续费提醒。Claude Code 可以固定 Date、推进伪计时器,并证明回调只触发一次;人必须审核时区、营业日、邮件或短信发送,以及可能影响账单的逻辑。
使用场景三是保存提示、校验消息、后台状态条这类小型 DOM 契约。Claude Code 可以用 jsdom 检查文本、角色属性和小快照;人必须审核真实浏览器行为、焦点移动、视觉回归和用户可见文案。
Claude Code 可做什么,人必须批准什么
| 范围 | 可以交给 Claude Code | 人必须批准 |
|---|---|---|
| 测试代码 | 起草模拟、伪计时器、jsdom 断言和覆盖率命令 | 确认断言没有削弱产品行为 |
| CI | 建议 vitest run 与 vitest run --coverage | 启用必跑检查、计费敏感 runner、生产部署规则 |
| 数据 | 使用 ord_1、book-1 这类合成 ID | 审核客户数据、密钥、API key、支付标识 |
| 浏览器行为 | 在 Vitest 中保留 DOM 契约 | 用 Playwright 或人工检查真实浏览器流程 |
常见陷阱与修复
| 失败模式 | 症状 | 修复 |
|---|---|---|
| 模拟没有恢复 | 调用次数或假实现泄漏到下一个测试 | 目的明确地使用 restoreMocks、vi.clearAllMocks() 或 vi.restoreAllMocks() |
| 伪计时器没有恢复 | 另一个文件的时间测试偶发失败 | 在 afterEach 中调用 vi.useRealTimers() |
| 把 jsdom 当真实浏览器 | CSS、布局、图像或 Canvas 与生产环境不同 | Vitest 检查 DOM 契约,Playwright 检查浏览器行为 |
| 快照太大 | 评审差异充满噪音 | 只快照小结构,关键属性直接断言 |
缺少 coverage.include | 未测试文件不可见 | 显式包含 src/**/*.{ts,tsx} |
| 异步没有等待 | 假阳性通过 | 使用 await expect(promise).resolves 或 rejects |
| 交互式 runner 进入监视模式 | 进程等待文件变化 | 使用 vitest run;变更文件用 vitest related --run |
修复的目标不是简单变绿。测试失败时,请粘贴完整错误,要求 Claude Code 不删除断言,先修实现或模拟设置。安全、生产、计费、客户数据相关修改必须等人工看过差异后再批准。
落地时建议先选一个风险最低的模块试运行:例如只读的订单摘要、后台状态提示或试用期日期计算。把 npm run test:run 和 npm run coverage 的输出贴给 Claude Code,让它解释红色分支来自输入校验、错误转换还是清理遗漏。随后由人确认差异没有碰到真实客户资料、生产环境变量、付费接口或部署配置。这样既能利用代理整理重复测试,又不会把安全和业务判断交给自动修改。
要在仓库中应用同样的评审规则,可使用 ClaudeCodeLab 的实用模板,其中包含测试标准、评审提示词和 CI 质量门。
实际验证结果
2026 年 7 月 22 日,文中的 TypeScript 示例被提取到临时项目,并用 Node v24.14.1 与 Vitest 4.1.10 执行。vitest run 和 vitest run --coverage 共通过 4 个文件、7 个测试,覆盖 API 边界、模块模拟、固定时钟、计时器边界、DOM 属性和内联快照。覆盖率为 statements 94.73%、branches 100%、functions 85.71%、lines 94.73%。Mailer 已被模拟,没有发送真实邮件,也没有调用生产 API。
相关文章
Claude Agent SDK 入门:把 Claude Code 安全嵌入应用
用最新 Claude Agent SDK 构建可审计的 AI 代理,涵盖权限、MCP、代码示例和坑点。
Claude Code 测试策略实战:Vitest、Testing Library、Playwright 与 CI
用 Claude Code 设计单元、集成、E2E 与 CI 测试,减少脆弱用例。
Claude Code 错误处理模式:在边界分类失败并设计恢复路径
用 Claude Code 设计可维护的错误处理。涵盖 API 校验、外部 API 失败、批处理失败与 TypeScript 实现。
免费 PDF: Claude Code 速查表
输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。
我们会妥善保护你的信息,不发送垃圾邮件。
让 Claude Code 真正进入可验证的工作流
先用免费 PDF 固定基础,再用 Gumroad 教材复用工作流;如果涉及团队导入、权限或收入路径,可以直接咨询。
关于作者
Masa
专注 Claude Code 实务流程、团队导入和内容转化的工程师。