Vitest — 复用 Vite 流水线的测试运行器
已复核Vitest 是一个把 Vite 开发服务器当成测试运行时的测试框架。日常类比:开发和测试共用同一条编译流水线——别再为测试单独养一套 Babel / ts-jest。
你写:
import { test, expect } from "vitest";test("add", () => { expect(1 + 1).toBe(2); });固定 4.1.11 启动时先找配置文件,再创建 Vite server,并把 Vitest 自己挂成插件。测试代码走 Vite 的模块图与 ServerModuleRunner,不是另起一套转译器。
不理解 Vitest 的运行时边界,下面这些事都解释不通:
- 为什么
resolve.alias/define/ Vite plugin 在测试里通常直接生效 - 为什么默认不是 jsdom,测 DOM 必须显式换 environment
- 为什么
vi.mock写在 import 后面仍然先于模块加载执行 - 为什么 browser mode 不是“开一个开关就有 Playwright”,而要另装 provider 包
固定源码的主链可以拆成四步:
-
找配置并启动 Vite:
createVitest()按vitest.config.*再vite.config.*的顺序搜索;找到后mergeConfig并注入VitestPlugin,再createViteServer()。 -
解析测试默认值:未写
pool时落到'forks';environment默认'node';isolate默认true;fileParallelism默认true;globals默认false。watch仅在非 CI、stdin 是 TTY、且不是 agent 时默认打开。 -
worker 跑 spec:默认 forks 池起子进程。每个文件在隔离环境里加载;模块执行默认走 Vite 的
ServerModuleRunner,除非把experimental.viteModuleRunner设成false。 -
watch 重跑的是受影响 spec:
VitestWatcher挂在vite.watcher的 change/unlink/add 上,把要重跑的测试文件记进changedTests。这是“判定哪些 spec 受影响再重跑”,不是 worker 内 HMR 热替换。
案例 1:显式 import 跑一条测试
Section titled “案例 1:显式 import 跑一条测试”import { test, expect } from "vitest";import { sum } from "./sum";
test("1 + 2 = 3", () => { expect(sum(1, 2)).toBe(3);});npx vitest run 一次跑完退出。没有配置文件时,仍用上面的默认 pool / environment。新项目不要开 globals: true——固定版本默认就是关的,显式 import 才能跳转和 tree-shake。
案例 2:mock 会被提升到文件顶
Section titled “案例 2:mock 会被提升到文件顶”import { test, expect, vi } from "vitest";import { fetchUser } from "./api";import { greet } from "./greet";
vi.mock("./api", () => ({ fetchUser: vi.fn(() => Promise.resolve({ name: "Jason" })),}));
test("greet uses mocked fetchUser", async () => { expect(await greet(1)).toBe("Hello, Jason"); expect(fetchUser).toHaveBeenCalledWith(1);});@vitest/mocker 的 hoistMocksPlugin 会把 vi.mock / vi.unmock / vi.hoisted 挪到文件顶部。factory 里引用“看起来写在前面、实际还没初始化”的变量会报 TDZ。要用 vi.hoisted(() => ...) 显式提升。
案例 3:coverage 默认关着
Section titled “案例 3:coverage 默认关着”npx vitest run --coverage配置默认 coverage.provider 是 'v8',但 enabled 是 false。打开后报告目录默认 ./coverage。Istanbul 是另一条 provider,且与 experimental.viteModuleRunner: false 不兼容。
-
默认 environment 是 node:测 DOM 要显式
environment: 'jsdom'或'happy-dom'。--dom会强制happy-dom,并覆盖配置里的其他 environment。 -
browser mode 要另装 provider:
browser.provider必须传入@vitest/browser-playwright/@vitest/browser-webdriverio/@vitest/browser-preview的工厂函数。只写字符串'playwright'不够;没指定 provider 会直接抛错。 -
Vitest 4 删掉了
test.poolOptions:旧的 per-pool 嵌套选项已提升成顶层字段。照着 3.x 文档抄poolOptions会被 deprecate 警告拦住。 -
watch 默认值看环境:CI、非 TTY 或 agent 环境下默认不 watch。把“本地默认 watch”当成所有调用约定会在 CI 里踩空。
适用 vs 不适用场景
Section titled “适用 vs 不适用场景”适用:
- 已经在用 vite 的项目——配置搜索会复用
vite.config.* - 需要 Jest 风格
describe/expect/vi.mock,但转译想跟 Vite 共用 - Node
^20 || ^22 || >=24,Vite^6 || ^7 || ^8
不适用:
- 纯 Webpack 栈、又不想引入 Vite——等于多装一条编译链
- 端到端真浏览器用户流——那是 playwright Test 的赛道;Vitest browser mode 测的是组件/单元,还要另装 provider
- 需要把静态阅读写成“比 Jest 快 N 倍”——本轮未跑对比 benchmark
固定版本边界
Section titled “固定版本边界”- 本文绑定
vitest-dev/vitest@9bd8d464...,即 lightweight tagv4.1.11,packages/vitest版本4.1.11。 - npm
vitest@4.1.11未暴露gitHead;以 GitHub tag 提交为溯源锚点。上游另有v5.0.0-rc.*,本文不绑定预发布线。 - 默认:
pool: 'forks'、environment: 'node'、isolate: true、fileParallelism: true、globals: false;coverage providerv8但默认关闭。 - 引擎:Node
^20.0.0 || ^22.0.0 || >=24.0.0;peer Vite^6.0.0 || ^7.0.0 || ^8.0.0。 - 本文未安装依赖、未运行上游测试、未测启动耗时或覆盖率开销,状态保持
UNVERIFIED。
- 测试运行器可以是 bundler 的 consumer——Vitest 把 Vite server 当宿主,而不是再养一套转译器
- 默认值必须按大版本重读:Vitest 4 默认 forks,且删掉了
poolOptions - hoist 是变换,不是书写位置:
vi.mock看起来写在后面,运行时仍在 import 之前 - browser mode 是可选 provider 插件,不是核心 runner 的默认池
- 不写
pool时,固定 4.1.11 默认用threads还是forks? vi.mock写在import后面,factory 里用到的顶层const一定已经初始化了吗?browser: { enabled: true }但不传provider,能启动 browser mode 吗?
检查点:
forks。resolveConfig在缺省时写resolved.pool ??= 'forks'。- 不一定。mock 会被提升到文件顶,factory 里的外部变量可能仍处在 TDZ,要用
vi.hoisted。 - 不能。未指定 provider 会要求从
@vitest/browser-playwright等包传入工厂函数。
- 官方文档:vitest.dev
- 固定源码:vitest-dev/vitest —— 本文绑定提交
9bd8d464e6328c567c2dbcd8fdd977d57a9425c2 - vite —— Vitest 的宿主编译流水线
- jest —— API 兼容的对照基准
- playwright —— 端到端测试;也是 Vitest browser provider 之一
- vite —— 理解 Vite server / ModuleRunner 才能理解 Vitest 为什么能复用 alias 和 plugin
- jest ——
describe/expect/ mock 命名的兼容目标 - playwright —— 真浏览器 e2e;Vitest browser mode 的 Playwright provider 与它不是同一条测试赛道
- esbuild —— Vite 依赖的转译器之一;不能单独解释 Vitest 4 的 runner
- bun ——
bun:test是另一条绑死 runtime 的测试入口