一个仓库中的测试不一定共享相同环境:后端工具需要 Node,组件需要 DOM,有的包还要运行独立的端到端测试。Vitest 的 projects 配置允许一个 Vitest 进程管理多份项目配置,既适合 monorepo,也适合给同一套代码指定不同的别名、插件或浏览器配置。
本文依据官方《Test Projects》全文翻译整理。原文未署个人作者;页面归属 VoidZero Inc. 与 Vitest contributors。2026-10-05 核对时,官网标示 Vitest 5.0.3,入门文档要求 Node ≥ 22.12.0、Vite ≥ 6.4.0。这里的版本是核对时的文档环境,不是文章首发日期。
旧资料中的 workspace 与本功能相同,但 workspace 自 3.2 起已弃用,由 projects 配置替代。尤其要注意第 5 版对内联项目默认继承的变化,不能把本文规则直接套用到旧版本。

从目录模式定义项目
在仓库根目录的 vitest.config.ts 中声明项目:
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
projects: ['packages/*'],
},
})
Vitest 会把 packages 下的各个目录视为独立项目,即使某个目录中没有配置文件。项目条目也可以是一个明确的配置文件路径、匹配多个项目的 glob 模式,或直接写在数组中的配置对象。
如果条目解析为文件,文件名需以 vitest.config 或 vite.config 开头,或者符合 vitest.<name>.config.*、vite.<name>.config.* 格式;<name> 可含字母、数字、下划线及连字符。例如:
vitest.config.ts
vite.config.js
vitest.unit.config.ts
vitest.e2e-node.config.ts
vite.e2e.config.js
vitest.config.unit.js
vite.config.e2e.js
可以用否定模式排除不应纳入的目录或文件:
export default defineConfig({
test: {
projects: [
'packages/*',
'!packages/excluded',
],
},
})
对于层级不齐的目录结构,要避免同时把中间层目录和它的子目录都当作项目。例如希望得到 packages/a、packages/b、packages/business/c 和 packages/business/d,却不希望把 packages/business 本身作为项目,可写成:
export default defineConfig({
test: {
projects: [
'packages/!(business)',
'packages/business/*',
],
},
})
若每个包区分单元测试与端到端测试配置,也可以直接匹配文件:
export default defineConfig({
test: {
projects: ['packages/*/vitest.config.{e2e,unit}.ts'],
},
})
该模式只选取扩展名前具有 e2e 或 unit 部分的配置文件。上面连续片段沿用同一个 defineConfig 导入;它们是不同配置方式,不应不加选择地堆在同一个文件里。
根配置不是自动运行测试的项目
声明 projects 后,根 vitest.config 不会自动被算作其中一个项目。若要运行根配置自身匹配的测试,必须明确把它列入项目定义。根配置仍承担进程级选项,例如报告器与覆盖率;内联项目则可按继承规则取得根配置中的项目相关选项。
根配置中的某些插件钩子仍然会执行,包括 apply、config、configResolved 和 configureServer。Vitest 也会使用这些插件来执行全局初始化与自定义覆盖率提供器。因此,“根不是一个测试项目”不能被理解为根配置中的代码不会运行。
混合外部项目与内联项目
同一个 projects 数组可以混用目录、文件与内联对象。下面保留原文的两个运行环境,使用更直接的文件名匹配表达式;这是编辑整理后的示例:
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
projects: [
'packages/*',
{
// Vitest 5:默认继承本配置中的选项
test: {
name: 'happy-dom',
environment: 'happy-dom',
include: ['tests/**/*.browser.test.{ts,js}'],
},
},
{
// 明确关闭内联配置继承
extends: false,
test: {
name: { label: 'node', color: 'green' },
environment: 'node',
include: ['tests/**/*.node.test.{ts,js}'],
},
},
],
},
})
运行前应在自己的项目中准备对应的测试文件与环境依赖。本文不附带这些业务测试,也没有安装 happy-dom 或验证该示例运行结果。
所有项目名称必须唯一,否则 Vitest 会报错。内联项目没有名称时会获得数字名称。由 glob 指定的项目,默认使用最近的 package.json 的 name,找不到时才用文件夹名。明确设置 name 能让日志和命令行筛选更易维护;对象形式还可以指定标签颜色。
项目配置并不支持全部根级选项。独立项目文件建议使用 defineProject,以获得更合适的类型检查:
// packages/a/vitest.config.ts
import { defineProject } from 'vitest/config'
export default defineProject({
test: {
environment: 'jsdom',
},
})
原文用在这个位置写 reporters: ['json'] 的方式展示类型错误。本文把可用配置与错误说明分开:报告器应写在根配置,而不是把原文用于示错的配置当作可直接采用的正常示例。
另一个容易忽略的细节是工作目录:即使项目设置了自己的 root,测试里的 process.cwd() 默认仍返回启动 Vitest 时所在的目录。读取测试夹具或生成输出文件时,不应未经核对就把它当作当前包的根目录。原文另有 Project Working Directory Does Not Change 说明。
运行全部项目或按名称筛选
在根 package.json 中定义脚本:
{
"scripts": {
"test": "vitest"
}
}
之后可通过 npm run test、yarn test、pnpm run test 或 bun run test 启动。这里的 Bun 命令需要保留 run,否则 bun test 会进入 Bun 自己的测试运行器。
若只想运行一个或几个项目,传入 --project。下面采用原文提供的 pnpm 形式:
pnpm run test --project e2e
pnpm run test --project e2e --project unit
--project 可重复使用,支持 * 通配和 ! 排除。项目必须不匹配任何否定模式;如果还提供了普通模式,则还需匹配其中至少一个:
# 以下是在已能调用本地 vitest 命令的环境中使用的示例
vitest --project '!e2e'
vitest --project 'unit*' --project '!unit (browser)'
测试筛选的名称以实际解析结果为准。单次执行而不监听文件变化可使用 vitest run;这是官方入门页中的补充说明,本文没有执行这些命令。
第 5 版的继承规则
内联配置从声明它的配置文件继承选项;对根级内联项目来说,这个来源就是根配置。该行为由 extends 控制,自 Vitest 5.0 起默认开启。下面的 unit 会继承 React 插件与线程池,integration 则明确关闭继承:
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
test: {
pool: 'threads',
projects: [
{
test: {
name: 'unit',
include: ['**/*.unit.test.ts'],
},
},
{
extends: false,
test: {
name: 'integration',
include: ['**/*.integration.test.ts'],
},
},
],
},
})
extends 还可以是另一个配置文件的路径。例如,在内联对象中写 extends: './vitest.shared.ts',可以改从该共享配置取得选项。
继承时会合并共享配置与项目自己的配置。像 setupFiles 这样的数组会拼接,而不是由项目数组整体覆盖。以下选项需要单独记住:
| 选项 | 继承规则 |
|---|---|
name、projects |
不继承。 |
根级 globalSetup |
不继承。它已经在每轮测试运行执行一次,若再传给每个项目会重复运行。 |
非根配置中的 globalSetup |
扩展该非根配置时仍会继承。 |
项目自己的 tags |
替换继承到的数组,不与其拼接。 |
通过配置文件路径或目录引用的项目,不会自动继承根配置。需要共享时,可建立共享配置,再在项目中显式合并:
// packages/a/vitest.config.ts
import { defineProject, mergeConfig } from 'vitest/config'
import configShared from '../vitest.shared.js'
export default mergeConfig(
configShared,
defineProject({
test: {
environment: 'jsdom',
},
}),
)
这个相对路径沿用原文目录结构:共享文件要确实位于 packages/vitest.shared.js。若你的共享配置在仓库根目录,应按实际目录调整,不能照抄路径后假设它指向根目录。
一些选项只允许在根配置中定义一次:coverage 面向整个进程;reporters 只使用根级报告器;resolveSnapshotPath 只认可根级解析器;attachmentsDir 指定所有项目共享的根级附件目录。其他不影响测试运行器的选项也可能不允许出现在项目配置中。官方配置参考会在这些选项旁给出相应标记。
通过高级 API 启动 Vitest 时,程序传入的配置也参与解析;该情况应继续核对官方 API 的项目配置解析说明,不能只靠文件继承规则推定全部结果。
嵌套项目及名称前缀
由文件路径引用的项目,或者包含配置文件的目录,可以在该配置文件中再次声明 projects。这类配置自身与根配置相似:只提供子项目,默认不运行自己的测试。
// 根目录 vitest.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
projects: ['./packages/app/vitest.config.ts'],
},
})
// packages/app/vitest.config.ts
import { defineProject } from 'vitest/config'
export default defineProject({
test: {
name: 'app',
projects: [
{
test: {
name: 'unit',
include: ['**/*.unit.test.ts'],
},
},
{
test: {
name: 'e2e',
include: ['**/*.e2e.test.ts'],
},
},
],
},
})
这会创建 app (unit) 与 app (e2e)。子项目的内联配置继承直接声明它们的 app 配置,而不是越过它直接继承仓库根配置;extends 路径也相对于这个声明配置解析。由于它属于非根配置,其中的 globalSetup 会按非根配置规则被继承。
筛选也认识前缀:--project app 会选择该配置下的项目,--project "app (unit)" 则只选一个。若还需要运行 app 配置自身的测试,在它的 projects 中增加对自身 './vitest.config.ts' 的引用:
// 片段:替换 app 配置中的 test 内容
test: {
name: 'app',
include: ['**/*.test.ts'],
projects: [
'./vitest.config.ts',
{
test: {
name: 'unit',
include: ['**/*.unit.test.ts'],
},
},
],
}
这段是配置对象内部的片段,不是单独完整文件。若父项目与子项目的 include 重叠,应确认同一测试按不同项目运行是否符合预期。只有配置文件能声明嵌套项目,内联配置内部不支持再放 projects。
用解析日志检查实际匹配结果
当某个项目没有被发现、名称与预期不同,或看起来共享了错误的服务器时,可以开启项目解析调试日志。原文的 shell 语法如下:
# POSIX shell 示例;不是 PowerShell 的赋值语法
DEBUG=vitest:projects vitest
日志会说明 glob 匹配了哪些路径、浏览器实例和 benchmark 项目如何展开、某个项目为何被 --project 排除,以及它创建独立 Vite 服务器还是复用其他项目的服务器。阅读时可按“发现路径 → 分配名称 → 继承配置 → 应用筛选 → 创建或复用服务器”的顺序核对。
本文没有贴出一段伪装为本次运行的日志。官方文档中的日志是用于解释这些阶段的示例;实际项目结果应在自己的受控测试环境里取得。
静态审查与运行边界
这些配置中没有发现硬编码密钥、动态拼接 shell 命令或明显注入构造。但配置文件、Vite 插件、setupFiles 和 globalSetup 都涉及代码执行,项目配置并不是不可信代码的安全沙箱。对外部仓库运行测试前,应先审阅这些入口,并使用不带生产凭据的隔离环境。
相对路径与 process.cwd()、数组拼接导致重复初始化、共享输出目录以及重叠的测试匹配范围,是本稿静态检查中重点标出的实际配置边界。本文未安装 Vitest、未执行构建或测试、未运行配置文件;不能把“未发现明显问题”解释为无漏洞或测试通过。
来源与许可
正文来源为 Vitest Test Projects;版本与依赖要求补充核对了 官网 和 Getting Started。保留署名:© 2026 VoidZero Inc. and Vitest contributors。
Vitest 仓库的 MIT License 另行适用于其软件及相关文档,许可原文完整保留在下方。本文原创图不使用原站截图。为保留复制代码所需的许可声明,下面附上 MIT 许可文本。
MIT License
Copyright (c) 2021-Present VoidZero Inc. and Vitest contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.












暂无评论内容