Vitest 使用指南:适用场景、核心能力与工程实践

This article is extracted from the chat log with AI. Please identify it with caution.

AI 参与说明(Agent:Codex):本文由 Codex 基于 Vitest 官方 v4.1 文档辅助整理,重点核对适用场景、Test Environment、Browser Mode、Mocking、Coverage、Type Testing、Test Projects 与 CI 能力。资料核验于 2026-08-26,Vitest 官方首页当日显示稳定版为 v4.1.11。最小示例已使用 Node.js 26.0.0、pnpm 10.14.0、Vitest 4.1.11 与 TypeScript 5.9.2 实际运行;读者仍应以项目 lockfile 和目标运行时为准。

适用范围:本文面向准备为 JavaScript / TypeScript 项目建立测试体系,或评估从 Jest 迁移到 Vitest 的开发者。Vitest 4 要求 Node.js 20+ 与 Vite 6+;即使项目不直接使用 Vite,也可以用 Vitest 测试 Node.js 代码。Getting Started Migrating to Vitest 4.0

结论#

如果项目以 JavaScript / TypeScript 为主,尤其已经使用 Vite、React、Vue 或 Svelte,Vitest 很适合作为默认的 Unit Testing 与轻量 Integration Testing 工具。它复用 Vite 的 Plugin、Alias、Module Resolution 与 Transformation Pipeline,提供 Jest-compatible 的 expect、Mock、Snapshot 和 Coverage API,并通过 Watch Mode 只重跑受改动影响的测试。Why Vitest Features

但 Vitest 不应被理解成“一个工具包办所有测试”:

  • 纯函数、业务规则、状态转换、Node.js Service 与 SDK,优先使用默认 node 环境。
  • UI Component 的轻量 DOM 测试可以使用 jsdomhappy-dom;涉及真实 CSS、Focus、Event Propagation 或 Browser API 时,使用 Browser Mode。
  • 完整登录、跨页面导航、支付、下载、多标签页或部署后验证,仍应由 Playwright、Cypress 等独立 End-to-End Testing 工具负责。
  • TypeScript 文件能被执行,不代表已经通过 Type Checking;应保留 tsc --noEmit,或另行启用 Vitest Type Testing。

Vitest 最直接的效果不是“证明系统没有 Bug”,而是把重要行为写成可重复执行的证据:开发期缩短修改后的反馈时间,重构时尽早发现 Regression,CI 中用退出码、Coverage Threshold 与机器可读 Report 阻止已知问题重新进入主分支。

场景决策表#

场景是否适合推荐方式能达到的效果
纯函数、价格计算、权限规则、解析与数据转换很适合node 环境的 Unit Test快速覆盖正常输入、边界值和错误分支
Node.js Service、SDK、Repository适合Mock 外部边界,并保留关键真实 Integration Test验证模块协作、错误传播与资源清理
Vite 驱动的 React / Vue / Svelte 应用很适合复用 Vite Config 与 Framework Plugin减少开发、构建和测试之间的转换差异
Component 状态、表单与基本 DOM 交互适合jsdom / happy-dom,或 Browser Mode验证渲染、交互、Loading 与 Error State
CSS Layout、Focus、Accessibility、真实 Browser API适合,但必须选对环境Browser Mode + Playwright Provider发现 DOM 模拟器无法覆盖的浏览器行为
TypeScript 公共 API、Generic 与类型推断适合*.test-d.ts + expectTypeOf / assertType把编译期 Type Contract 变成 Regression Test
Monorepo、多个 Package、Node 与 Browser 混合测试适合test.projects用独立 Config 管理多种 Test Strategy
大型 CI Test Suite适合Worker Parallelism、Filter、Shard、Coverage 与 Reporter缩短 CI 时间,并生成 JUnit / JSON / HTML 结果
稳定的小型结构化输出有条件适合Snapshot,并审阅每次 Snapshot Diff发现非预期结构变化
完整 End-to-End User Journey只能补充Vitest 负责 Unit / Component,Playwright / Cypress 负责 E2E覆盖真实部署、页面跳转与 Browser Context
非 JavaScript / TypeScript 项目不适合使用对应语言的 Testing Framework避免引入无关 Toolchain

Vitest 到底负责什么#

Vitest 首先是 Test Runner,但它把常见测试能力组合到了一条工程链路中:

  1. 根据 *.test.**.spec.* 或自定义 Pattern 发现 Test File。
  2. 通过 Vite 的 Transformation Pipeline 处理 ESM、TypeScript、JSX、Alias 与 Plugin。
  3. 在 Worker 中执行 Test File;不同文件默认并行,同一文件内的 Test 默认顺序执行。
  4. expect 进行 Assertion,用 vi 创建 Mock、Spy、Fake Timer 和 Stub。
  5. 输出 Terminal、JUnit、JSON、HTML、Coverage 或 Vitest UI 结果。
能力常用 API / 配置解决的问题
Test 与 Suitetestitdescribetest.each描述 Example、分组与参数化 Case
Assertionexpect.resolves.rejectsexpectTypeOf验证值、错误、异步结果与 Type Contract
LifecyclebeforeEachafterEachbeforeAllafterAll建立并回收 Test Resource
Test Doublevi.fnvi.spyOnvi.mock隔离 Network、Clock、Environment 与外部 Service
Time Controlvi.useFakeTimersvi.setSystemTime稳定测试 Timeout、Retry、Debounce 与日期逻辑
SnapshottoMatchSnapshottoMatchInlineSnapshottoMatchFileSnapshot固化稳定的结构化输出
Environmentnodejsdomhappy-dom、Browser Mode在不同 Runtime Fidelity 与速度之间取舍
CoverageV8 / Istanbul Provider、Threshold找出没有执行到的 Statement、Branch 与 Function
ScaleWatch、Filter、Test Tags、Projects、Shard控制大仓库的反馈时间与 CI 负载
ReportDefault、JUnit、JSON、HTML、Vitest UI为开发者、CI 与外部平台提供结果

Vitest 还提供 Benchmarking,但官方将其标记为 Experimental。它适合比较同一环境中的实现差异,不应替代 Production Profiling 或完整的 Performance Testing。Benchmarking

先选对 Test Environment#

环境选错会让 Test 看似通过,却没有验证真正关心的 Runtime Behavior。

环境特征适合场景主要边界
node默认、启动快,没有 DOM业务逻辑、Node.js Service、CLI、SDK不能证明 Browser API 与 UI Behavior
jsdom在 Node.js 中模拟较完整 DOMComponent 的基本 Render、Form 与 Event没有真实 Layout Engine,可能出现 False Positive / Negative
happy-dom更轻量的 DOM 模拟API 需求较少、追求启动速度的 Component TestWeb API 覆盖通常少于真实 Browser
Browser ModeTest 原生运行在 BrowserCSS、Focus、Accessibility、Canvas、真实 Event 与 Browser API初始化更慢,官方仍建议 Critical Flow 补充独立 Browser Test Runner

Vitest 官方目前推荐用 Browser Mode 做 Component Testing,因为真实 Browser 能覆盖 CSS Layout、Event Propagation、Focus Management 与 Accessibility 等模拟环境容易遗漏的行为。Why Browser Mode Component Testing

Browser Mode 不是 environment: "browser"。它需要 Provider,并通过独立的 browser 配置启用:

pnpm add -D @vitest/browser-playwright@4.1.11 playwright
pnpm exec playwright install chromium
// vitest.config.ts
import { playwright } from "@vitest/browser-playwright";
import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    browser: {
      enabled: true,
      provider: playwright(),
      instances: [{ browser: "chromium" }],
    },
  },
});

可以用 vitest init browser 生成初始配置。CI 中应使用 Playwright 或 WebdriverIO Provider;官方把 Preview Provider 定位为预览用途,并推荐没有既有选择时优先采用支持并行执行的 Playwright Provider。Browser Mode

最小可复现示例#

下面用一个报价模块同时展示同步 Assertion、Async Test、vi.fnvi.spyOn、Error Test 与 Coverage Threshold。

1. 前置条件与安装#

示例按 Vitest 4.1.11 编写,要求 Node.js 20+。已有 Vite 项目还需要确认 Vite 为 6+。

pnpm add -D vitest@4.1.11 @vitest/coverage-v8@4.1.11 typescript@5.9.2

package.json 中加入:

{
  "scripts": {
    "test": "vitest",
    "test:run": "vitest run",
    "test:coverage": "vitest run --coverage"
  }
}
  • pnpm test 用于本地开发,在交互终端默认进入 Watch Mode。
  • pnpm test:run 只执行一次后退出,适合 CI。
  • pnpm test:coverage 执行一次并生成 Coverage Report。

2. 配置 Vitest#

// vitest.config.ts
import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    include: ["src/**/*.test.ts"],
    environment: "node",
    clearMocks: true,
    restoreMocks: true,
    coverage: {
      provider: "v8",
      include: ["src/**/*.ts"],
      exclude: ["src/**/*.test.ts", "src/**/*.test-d.ts"],
      reporter: ["text", "html"],
      thresholds: {
        statements: 90,
        branches: 80,
        functions: 90,
        lines: 90,
      },
    },
  },
});

这里有三个重要取舍:

  • Vitest 4 默认只在 Coverage Report 中展示 Test Run 实际加载的文件。显式设置 coverage.include,才能让完全没有被 Test 导入的源文件以 0% 出现在 Report 中。
  • 不设置 coverage.enabled: true,避免本地普通 Watch Run 每次都承担 Coverage 开销;只在 test:coverage 中用 --coverage 开启。
  • 90 / 80 / 90 / 90 只是演示 Threshold 的写法,不是通用质量标准。现有项目应先测出 Baseline,再逐步提高,而不是为了数字编写没有行为价值的 Assertion。

Vitest 4 已移除旧教程常见的 coverage.all。升级时应删除该项,并用 coverage.include 明确 Source Scope。Coverage Coverage Config Vitest 4 Migration

3. 编写待测模块#

// src/pricing.ts
export interface PriceClient {
  getPrice(sku: string): Promise<number>;
}

export interface AuditLog {
  record(message: string): void;
}

export function calculateSubtotal(
  unitPrice: number,
  quantity: number,
): number {
  return unitPrice * quantity;
}

export async function createQuote(
  client: PriceClient,
  audit: AuditLog,
  sku: string,
  quantity: number,
) {
  const unitPrice = await client.getPrice(sku);
  const total = calculateSubtotal(unitPrice, quantity);

  audit.record(`quoted:${sku}:${total}`);

  return {
    sku,
    quantity,
    unitPrice,
    total,
  };
}

4. 编写 Test#

// src/pricing.test.ts
import { describe, expect, it, vi } from "vitest";
import { calculateSubtotal, createQuote } from "./pricing";

describe("calculateSubtotal", () => {
  it("calculates a subtotal", () => {
    expect(calculateSubtotal(25, 2)).toBe(50);
  });
});

describe("createQuote", () => {
  it("reads a price and records an audit event", async () => {
    const getPrice = vi.fn(async (_sku: string) => 25);
    const audit = {
      record(_message: string): void {},
    };
    const recordSpy = vi.spyOn(audit, "record");

    await expect(
      createQuote({ getPrice }, audit, "sku-1", 2),
    ).resolves.toEqual({
      sku: "sku-1",
      quantity: 2,
      unitPrice: 25,
      total: 50,
    });

    expect(getPrice).toHaveBeenCalledOnce();
    expect(getPrice).toHaveBeenCalledWith("sku-1");
    expect(recordSpy).toHaveBeenCalledWith("quoted:sku-1:50");
  });

  it("propagates service errors without writing an audit event", async () => {
    const getPrice = vi.fn(async () => {
      throw new Error("price service unavailable");
    });
    const audit = { record: vi.fn() };

    await expect(
      createQuote({ getPrice }, audit, "sku-1", 2),
    ).rejects.toThrow("price service unavailable");

    expect(audit.record).not.toHaveBeenCalled();
  });
});

这个 Test 刻意先用 Dependency Injection,而不是 vi.mock 整个 Module:

  • vi.fn 创建可控的 PriceClient Test Double,并记录调用次数与参数。
  • vi.spyOn 观察已有 Method,默认仍执行原 Implementation。
  • .resolves.rejects 分别验证 Async Success 和 Failure。
  • 失败场景额外断言 Audit 没有写入,避免只检查 Error Message 而漏掉 Side Effect。

vi.mock 适合无法直接注入的 Module Boundary,但它会被提升到 Import 之前;复杂 Mock 还受 ESM 与 Browser Mode 限制。优先通过清晰的 Interface 与 Dependency Injection 缩小 Mock 范围,通常更容易理解和维护。Mocking Mocking Modules

5. 运行与验证#

pnpm test:run
pnpm test:coverage

预期核心结果:

Test Files  1 passed (1)
Tests       3 passed (3)

Statements : 100% (5/5)
Branches   : 100% (0/0)
Functions  : 100% (2/2)
Lines      : 100% (5/5)

这里的 100% 只表示这个小例子中的可执行代码被执行,并不证明设计、断言或业务需求完整。

已有 Vite Config 时如何处理#

Vitest 默认读取 vite.config.*,所以已有的 Alias、React Plugin 与 Transformation Config 可以直接复用。最简单的做法是在同一份 Config 中加入 test

选择 jsdom Environment 时,需要先安装对应 Package:

pnpm add -D jsdom
// vite.config.ts
import { fileURLToPath, URL } from "node:url";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vitest/config";

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      "@": fileURLToPath(new URL("./src", import.meta.url)),
    },
  },
  test: {
    environment: "jsdom",
  },
});

如果新增独立的 vitest.config.*,它的优先级更高,Vitest 不会再自动合并原来的 vite.config.*。需要复用原配置时,应使用 mergeConfig 显式合并;如果原配置导出 Config Function,则应在对应 Function 内完成 Merge。Configuring Vitest Vite resolve.alias

日常开发与 CI 怎么用#

目标命令说明
本地持续反馈pnpm testWatch Mode 根据 Module Graph 重跑 Related Test
一次性执行pnpm test:runCI 应显式使用 Run Mode,不依赖 Terminal Detection
只跑一个文件pnpm exec vitest src/pricing.test.tsFile Argument 按 Path Substring 筛选
只跑一个 Testpnpm exec vitest src/pricing.test.ts -t "service errors"大仓库同时传 File Path,避免加载无关 Test File
运行受 Source 影响的 Testpnpm exec vitest related src/pricing.ts --run适合 Pre-commit 或增量检查
生成 Coveragepnpm test:coverageThreshold 未达标时返回非零 Exit Code
只运行 Type Testpnpm exec vitest --typecheck.only运行 *.test-d.ts;需要有效的 tsconfig.json
Test UIpnpm exec vitest --ui需要额外安装 @vitest/ui
CI Shardpnpm exec vitest run --shard=1/3不可与 Watch Mode 同时使用

Watch Mode 的核心收益是利用 Module Graph 找到受改动影响的 Test,而不是每次从头执行整个 Test Suite。随着仓库增大,还可以结合 Filename、-t、Line Number、Related Test 与 Test Tags 过滤。CLI Test Filtering

Vitest 4.1 引入 Test Tags。Tag 必须先在 Config 中定义,再用 --tags-filter 表达式筛选;它适合区分 unitintegrationslow 等稳定 Test Class,但不应取代清晰的文件结构。Test Tags

CI 的最小 Quality Gate 通常包含:

- run: pnpm install --frozen-lockfile
- run: pnpm exec tsc --noEmit
- run: pnpm test:coverage

如果项目用 *.test-d.ts 固化公共 Type Contract,再增加 pnpm exec vitest --typecheck.only。其中 tsc --noEmit 检查整个 Project 的类型,Vitest Type Testing 则执行专门的 Type Test;两者关注点不同。

当 Test Suite 增大时,可以增加:

  • JUnit / JSON Reporter,供 CI 平台解析 Test Result。
  • Test HTML Reporter 或 Vitest UI Artifact,供人工查看 Test Failure;两者需要 @vitest/ui
  • Shard,把 Test File 分散到多个 CI Job,再用 Blob Reporter 合并结果。
  • test.projects,分别运行 Unit、Integration、DOM 与 Browser Config。

Test HTML Reporter 与前文 coverage.reporter: ["text", "html"] 不是同一份输出:前者报告 Test Result,后者由 Coverage Provider 生成 Coverage HTML Report。

Reporters Vitest UI Test Projects

几类能力应怎样使用#

Mock:隔离不稳定边界,不要伪造整个世界#

vi 可以 Mock Function、Module、Date、Timer、Global、Environment Variable、File System 与 Request。适合隔离第三方 API、Clock、Randomness 和失败重试,但 Mock 只能证明“代码按预期调用了 Test Double”,不能证明双方真实 Protocol 兼容。

关键 HTTP Contract、Database Migration、Queue Message 或 Storage Behavior 应保留少量真实 Integration Test。每个 Test 后应 Clear 或 Restore Mock,避免 State 泄漏到其他 Case。

Coverage:寻找盲区,不把百分比当正确性#

Vitest 支持 V8 与 Istanbul Provider:

  • V8 是默认选项,通常执行更快、Memory Overhead 更低;Vitest 3.2 起通过 AST Remapping 提供与 Istanbul 对齐的 Source Coverage 精度。
  • Istanbul 在执行前 Instrument Source,适用于非 V8 JavaScript Runtime,但有额外的 Transform 与 Runtime Overhead。

Coverage 应回答“哪些 Branch 与 Error Path 从未执行”,而不是替代 Code Review、Integration Test、Security Test 或需求评审。Vitest 官方还明确说明 V8 Provider 不能用于 Firefox、Bun、Cloudflare Workers 等不暴露 V8 Coverage Profiler 的环境。Coverage

Snapshot:固定稳定 Contract,不盲目按 u#

Snapshot 适合小而稳定、Diff 容易审阅的 Object、Serializer Output、HTML Fragment 或 Generated File。Snapshot File 应提交到 Version Control,并像 Source Code 一样 Review。

如果 Snapshot 很大、包含随机 ID、时间、平台相关路径或频繁变化的文案,开发者容易机械执行 vitest -u,从而把 Regression 一并批准。此时用针对 Behavior 的 Assertion 通常更清晰。Snapshot

Type Testing:验证 Type Contract,不替代 Runtime Test#

Vitest 默认把 *.test-d.ts 识别为 Type Test,并通过 expectTypeOfassertType 与 TypeScript Compiler 验证公共 API:

// src/pricing.test-d.ts
import { expectTypeOf, test } from "vitest";
import { createQuote } from "./pricing";

test("createQuote keeps its async return contract", () => {
  expectTypeOf(createQuote).returns.toEqualTypeOf<Promise<{
    sku: string;
    quantity: number;
    unitPrice: number;
    total: number;
  }>>();
});

Type Test 只做 Static Analysis,不执行 Function Body。--typecheck 会在 Runtime Test 之外启用 Type Test,--typecheck.only 则只运行 Type Test;两者都需要有效的 tsconfig.json。公共 Library 可以同时保留 Runtime Test 与 *.test-d.ts,分别验证 Behavior 和 Type Inference。

截至 Vitest 4.1.11,CLI 仍将内置 Type Checking 标记为 Experimental,并建议 Pin Vitest Version。因此应把它视为补充能力,保留独立的 Project-level Type Check,并在升级 Vitest 时复核行为。Testing Types expectTypeOf API CLI

Parallelism、Projects 与 Sharding:先隔离,再加速#

Vitest 默认并行执行不同 Test File;Vitest 4 的默认 Pool 为 forks。同一 Test File 内默认顺序执行,只有显式使用 test.concurrent 才会并发。Parallelism

这意味着共享 Database、Port、Filesystem Directory 或 Test Account 可能发生冲突。正确顺序是先为每个 Worker / Test 创建独立 Resource,再提高 maxWorkers 或启用更多 Concurrent Test;无法隔离时才使用 fileParallelism: false

Monorepo 或多环境项目使用 test.projects。旧名称 workspace 自 Vitest 3.2 起已 Deprecated,不应继续复制旧 Config。Test Projects

Cloudflare Workers 的特殊说明#

默认 node Environment 不能证明 Worker Runtime API、Binding、Durable Object 或隔离 Storage 的行为正确。Cloudflare 当前推荐使用官方 @cloudflare/vitest-plugin,让 Vitest Test 在 Workers Runtime 中运行,并提供 Binding Access、每个 Test File 隔离的 Storage、Multi-Worker Project 与本地 Miniflare Execution。当前前置条件包括 Vitest 4.1+、ES Modules Worker,以及 compatibility_date 不早于 2022-10-31Cloudflare Workers Vitest integration Write your first test

Workers Runtime 不支持 Vitest 的 V8 Coverage Provider;需要 Coverage 时,应安装 @vitest/coverage-istanbul 并把 Provider 配置为 istanbul,不能直接套用前文的 Node.js V8 示例:

pnpm add -D @vitest/coverage-istanbul@4.1.11

此外,Vitest Fake Timers 不会推进 KV、R2 与 Cache Simulator 使用的内部时间,因此不能通过调整 Fake Clock 测试这些服务的 TTL 到期。旧的 @cloudflare/vitest-pool-workers 应按官方 Migration Guide 迁移到新 Plugin。Known issues Migrate to the Vitest integration

迁移与配置边界#

  1. Vitest 与 Jest API Compatible,但不是完全等价。迁移时应重点检查 ESM、Module Mock、Fake Timer、Hook、Snapshot 与 Jest Plugin,不要只替换 Command Name。Migrating from Jest
  2. Browser Mode 仍处于较早阶段。官方建议关键 Browser Flow 继续由 Playwright、Cypress 或 WebdriverIO 等独立 Runner 补充。Why Browser Mode
  3. 独立 vitest.config.* 不会自动继承 vite.config.*。依赖现有 Alias 或 Plugin 时,应放在同一 Config,或显式 Merge。

推荐的落地顺序#

  1. 从最稳定、最有业务价值的 Pure Function 和 Regression Bug 开始,不先追求 Test 数量。
  2. 为 Network、Time、Randomness 与 Storage 建立清晰 Boundary,优先 Dependency Injection,再考虑 Module Mock。
  3. 在 CI 使用 vitest run,同时保留 Type Check;不要让 Watch Mode 留在 CI 中等待。
  4. 显式配置 coverage.include,记录当前 Baseline,再为关键目录设置可达到的 Threshold。
  5. 为关键 HTTP、Database、Queue 与 Runtime Adapter 增加少量真实 Integration Test。
  6. UI Test 先判断是否需要真实 Browser;简单 Behavior 使用 DOM 模拟,Browser-specific Behavior 使用 Browser Mode。
  7. Test Suite 增长后再引入 Projects、Tags、Reporter 与 Shard;每次并行化前先保证 Resource Isolation。

这样建立起来的测试体系通常会形成三层:

层级数量倾向主要目标推荐工具
Unit / Module快速验证规则、边界与错误Vitest node
Component / Integration适中验证模块协作与 UI BehaviorVitest + DOM Environment / Browser Mode / Runtime Plugin
End-to-End少而关键验证完整 User Journey 与部署系统Playwright / Cypress 等独立 Runner

延伸阅读#

官方资料#

本文共 7324 字,创建于 Aug 26, 2026

相关标签: Frontend, TypeScript, Tools, ByAI