mcpField
W
web-testing-vitest

web-testing-vitest

github.com/agents-inc测试与质量2689 次安装2 天前更新
代码仓库

关于 web-testing-vitest

web-testing-vitest 是一份面向使用 Vitest 的开发者的测试模式指南,涵盖测试结构、mock、断言和配置,并强调版本差异与常见错误。

主要功能

  1. 01测试结构与选项写法
  2. 02函数替身、间谍与模块 mock
  3. 03假计时器及时间相关测试
  4. 04断言、异步检查与快照
  5. 05环境、setup 文件与多项目配置
  6. 06Vitest 3/4 迁移提示

适用场景

  1. 01为纯逻辑代码编写测试
  2. 02测试依赖 DOM 的代码
  3. 03选择模块 mock 或 HTTP 边界拦截
  4. 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.yaml
reference.md
examples/
anti-patterns.md
core.md
integration.md

详细介绍

Vitest 测试运行器模式

快速指南: Vitest 使用与应用构建相同的转换流程和配置来运行测试,因此别名、插件和 TypeScript 无需额外工具链即可正常工作。使用 describe/it/expect 组织测试,使用 vi.fn() 和 vi.spyOn() 创建测试替身,使用 vi.mock() 模拟整个模块,使用 vi.useFakeTimers() 处理所有依赖时钟的逻辑。Vitest 4 是当前稳定版(Vite 6+、Node 20+),并更改了若干模拟默认行为——vi.restoreAllMocks() 不再重置自动模拟,且 poolOptions 已移除。

详细资源:


适用路径

  • 纯逻辑,不涉及 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>

评论

支持 Markdown