跳至正文
Tooling — Vitest 使用指南:适用场景、核心能力与工程实践

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

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 配置启用:

bash
pnpm add -D @vitest/browser-playwright@4.1.11 playwright
pnpm exec playwright install chromium
ts
// 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+。

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

package.json 中加入:

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

ts
// 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. 编写待测模块

ts
// 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

ts
// 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. 运行与验证

bash
pnpm test:run
pnpm test:coverage

预期核心结果:

text
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:

bash
pnpm add -D jsdom
ts
// 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 通常包含:

yaml
- 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:

ts
// 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 示例:

bash
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

延伸阅读

官方资料

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

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

博客助手

正在打开博客助手…