关于 web-testing-vitest
web-testing-vitest 是一份面向使用 Vitest 的开发者的测试模式指南,涵盖测试结构、mock、断言和配置,并强调版本差异与常见错误。
主要功能
- 01测试结构与选项写法
- 02函数替身、间谍与模块 mock
- 03假计时器及时间相关测试
- 04断言、异步检查与快照
- 05环境、setup 文件与多项目配置
- 06Vitest 3/4 迁移提示
适用场景
- 01为纯逻辑代码编写测试
- 02测试依赖 DOM 的代码
- 03选择模块 mock 或 HTTP 边界拦截
- 04排查 Vitest 4 配置和 mock 行为
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ agents-inc/ skills --skill web-testing-vitest安装后在 Claude Code 中输入/web-testing-vitest确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
metadata.yamlreference.mdexamples/anti-patterns.mdcore.mdintegration.md详细介绍
Vitest 测试运行器模式
快速指南: Vitest 使用与应用构建相同的转换流程和配置来运行测试,因此别名、插件和 TypeScript 无需额外工具链即可正常工作。使用
describe/it/expect组织测试,使用vi.fn()和vi.spyOn()创建测试替身,使用vi.mock()模拟整个模块,使用vi.useFakeTimers()处理所有依赖时钟的逻辑。Vitest 4 是当前稳定版(Vite 6+、Node 20+),并更改了若干模拟默认行为——vi.restoreAllMocks()不再重置自动模拟,且poolOptions已移除。
详细资源:
- examples/core.md — 测试结构和选项、间谍、模块模拟、虚拟计时器、断言和快照
- examples/integration.md — 设置文件、生命周期钩子、环境,以及在传输边界处进行模拟
- examples/anti-patterns.md — 哪些内容不应该编写测试
- reference.md — v3/v4 迁移说明、配置和模拟 API 速查
适用路径
- 纯逻辑,不涉及 DOM — 使用默认的
environment: "node",无需设置文件;参阅 examples/core.md。 - 会访问
document或window的代码 — 设置environment: "jsdom"(或happy-dom)并添加设置文件;参阅 examples/integration.md。 - 同一仓库中有多个使用不同环境的测试套件 — 每个环境添加一个
projects条目,这样单次运行就能覆盖所有套件;参阅 reference.md。
<critical_requirements>
编写 Vitest 代码前
将测试选项作为第二个参数传入——test("name", { timeout: 10_000 }, () => {})。v2 之后已移除末尾选项的写法,而且混用选项对象和末尾超时设置会被直接拒绝。
返回被测文件从 vi.mock() 工厂导入的所有导出。 工厂会替换整个模块,因此未包含的内容在导入时都会是 undefined;可以使用 importOriginal() 保留其余内容,并记住该调用会被提升到文件中的所有导入之前。
在测试之间重置模拟状态——在配置中设置 restoreMocks: true,或在 afterEach 中显式调用 vi.resetAllMocks()。在 v4 中,vi.restoreAllMocks() 只会处理手动创建的间谍,因此自动模拟的模块不会因该调用而重置状态。
在代码实际经过的边界处进行模拟。 模块模拟会将测试绑定到导入图;任何移动函数的重构都会导致测试失效。拦截 HTTP 请求则不会限制导入图,还能覆盖序列化过程。
</critical_requirements>
自动检测: Vitest、vitest.config、vi.fn、vi.mock、vi.spyOn、vi.hoisted、vi.mockObject、vi.useFakeTimers、vi.advanceTimersByTime、mockResolvedValue、mockRejectedValue、importOriginal、toMatchInlineSnapshot、expect.schemaMatching、defineConfig test 块、projects、coverage provider
适用于:
- 运行和配置测试运行器——环境、设置文件、
projects、覆盖率 - 测试替身:间谍、存根、模块模拟、模拟计时器和模拟全局对象
- 断言、异步断言和快照
- 决定哪些内容值得编写测试,以及应在哪个层级进行测试
其他部分负责:
- 渲染组件并查询结果——测试运行器提供环境;在其中挂载什么,则由负责组件测试的工具决定
- 定义 HTTP 处理程序并运行拦截服务器——本技能介绍它们在测试生命周期中的启动和重置时机,而非编写方式
- 通过真实浏览器驱动用户流程——这需要使用不同的工具,也有不同的反馈循环
Vitest 会读取项目自身的 Vite 配置,因此测试解析导入、别名和插件的方式与应用完全相同。这正是它本身几乎不需要配置的原因,也意味着遇到失败时,通常需要检查配置(“此测试套件使用什么环境运行?”),而不是测试运行器。
<decision_framework>
选择哪种测试替身:
- 协作者是你已有引用的函数 → 使用
vi.spyOn(obj, "method"),它会保留原始实现以便还原。 - 协作者是传入的 → 使用
vi.fn()配合mockReturnValue/mockResolvedValue;不需要模块相关机制。 - 协作者通过 HTTP 访问 → 拦截请求而非模拟模块,这样测试会覆盖序列化过程,也能经受重构。
- 协作者是完全没有接缝的模块(文件系统、时钟、加密)→ 使用
vi.mock()模拟模块,使用vi.useFakeTimers()模拟时钟,使用vi.stubGlobal()模拟全局对象。
</decision_framework>
核心模式
模式 1:测试结构和选项
选项作为第二个参数传入。test.each 无需编写循环即可覆盖表格驱动的用例,it.concurrent 则会在同一文件中并行运行同级测试。
describe("formatCurrency", () => {
it("formats the default currency", () => {
expect(formatCurrency(1234.56)).toBe("$1,234.56");
});
it("retries a known-flaky path", { retry: 2, timeout: 10_000 }, async () => {
await expect(fetchRate()).resolves.toBeGreaterThan(0);
});
it.each([
[0, "$0.00"],
[-1, "-$1.00"],
])("formats %d as %s", (input, expected) => {
expect(formatCurrency(input)).toBe(expected);
});
});
完整代码:examples/core.md
模式 2:间谍和存根
vi.fn() 会创建独立的测试替身;vi.spyOn() 会包装已有方法,并可还原。两者都会在 .mock 中记录调用。
const onSave = vi.fn().mockResolvedValue({ id: "1" });
await submit({ onSave });
expect(onSave).toHaveBeenCalledWith({ title: "Draft" });
const spy = vi.spyOn(clock, "now").mockReturnValue(0);
stampEvent("saved");
expect(spy).toHaveBeenCalledTimes(1);
spy.mockRestore();
完整代码:examples/core.md
模式 3:模块模拟
vi.mock() 会被提升到导入语句之前,因此其工厂无法访问文件主体中的任何内容,除非该值来自 vi.hoisted()。importOriginal 可以保留测试未替换的导出。
const { readConfig } = vi.hoisted(() => ({ readConfig: vi.fn() }));
vi.mock("./config", async (importOriginal) => ({
...(await importOriginal<typeof import("./config")>()),
readConfig,
}));
readConfig.mockReturnValue({ locale: "en-US" });
完整代码:examples/core.md
模式 4:模拟计时器
安装模拟计时器,显式推进时间,并在之后恢复真实计时器,以免影响后续测试套件。
beforeEach(() => vi.useFakeTimers());
afterEach(() => vi.useRealTimers());
it("fires the debounced callback once", () => {
const onIdle = vi.fn();
watchIdle(onIdle, 300);
vi.advanceTimersByTime(300);
expect(onIdle).toHaveBeenCalledOnce();
});
完整代码:examples/core.md
模式 5:断言和快照
toStrictEqual 会同时比较结构和值;resolves / rejects 让异步断言保持在一个表达式中;内联快照则将预期值放在测试旁边。
await expect(loadUser("missing")).rejects.toThrowError(/not found/);
expect(toSummary(order)).toStrictEqual({ total: 118, items: 2 });
expect(parseArgs(["--json"]).json).toMatchInlineSnapshot(`true`);
完整代码:examples/core.md
模式 6:配置和环境
test 块位于项目自己的配置文件中。environment 决定测试套件可用的全局变量,setupFiles 会在每个测试文件运行前执行,而 projects 让一次运行可以使用多个环境和配置。
import { defineConfig } from "vitest/config"; // not "vite" — that one has no `test` key
export default defineConfig({
test: {
environment: "node",
setupFiles: ["./tests/setup.ts"],
restoreMocks: true,
coverage: { provider: "v8", include: ["src/**/*.ts"] },
},
});
完整代码:examples/integration.md — 参阅 reference.md 中的查找表
<red_flags>
危险信号
运行时会出错:
- 在测试主体之后传入选项——
test("x", fn, { retry: 2 })——此用法在 v2 之后已移除;请将选项放在第二个参数中。 vi.mock()工厂引用文件主体中的const——该调用会被提升,因此绑定处于暂时性死区;请从vi.hoisted()获取该值。- 工厂只返回被测试的导出项——所有其他导出项都会变成
undefined;请先展开await importOriginal()。 - 替换默认导出时未使用
default键——工厂的返回结构就是模块的结构,因此必须使用default: vi.fn()。 - v4 配置中的
poolOptions——已移除;maxWorkers和isolate是顶层选项。 - v4 配置中的
coverage.all——已移除;现在必须使用coverage.include,才能报告任何内容。
容易引起意外的行为:
- v4 中的
vi.restoreAllMocks()只会恢复手动创建的 spy;对于自动模拟的模块,需要使用vi.resetAllMocks()。 - v4 中
vi.fn().mock.invocationCallOrder从1开始,而此前从0开始。 vi.fn().getMockName()返回的是"vi.fn()"而不是"spy",这会改变捕获了该值的快照。- 自动模拟的 getter 会返回
undefined,而不会调用原 getter。 - 留下的模拟计时器会泄漏到同一 worker 中后续的文件——每次调用
useFakeTimers()时,都要配对调用useRealTimers()。 - 拦截服务器的处理程序是进程全局的;请在每个测试后重置它们,否则某个测试的覆盖设置会处理下一个测试的请求。
</red_flags>