进阶 vitest.dev 2026-10-22 19:00:41 · 20 阅读

第22章 Vitest 常见错误解析与解决方案

常见错误

找不到模块 './relative-path'

如果收到“找不到模块”的错误,可能意味着以下几种情况:

  • 路径拼写错误。请确保路径正确。
  • 你可能在 tsconfig.json 中依赖了 baseUrl。Vite 默认不会读取 tsconfig.json,因此如果你依赖此行为,可能需要自行安装 vite-tsconfig-paths。
    ts
    import { defineConfig } from 'vitest/config'
    import tsconfigPaths from 'vite-tsconfig-paths'

    export default defineConfig({
    plugins: [tsconfigPaths()]
    })

    或者重写路径,使其不以根目录为相对基准:
    diff
    - import helpers from 'src/helpers'
    + import helpers from '../src/helpers'
  • 确保没有使用相对于根目录的别名。Vite 将别名视为相对于导入文件的位置,而非项目根目录。
    ts
    import { defineConfig } from 'vitest/config'

    export default defineConfig({
    test: {
    alias: {
    '@/': './src/',
    '@/': new URL('./src/', import.meta.url).pathname,
    }
    }
    })

无法终止 Worker

当使用 pool: 'threads' 并配合 NodeJS 的 fetch 时,可能会发生此错误。详见 #3077。默认的 pool: 'forks' 没有这个问题。如果你显式设置了 pool: 'threads',改回 'forks' 或使用 'vmForks' 即可解决。

项目工作目录未改变

在多项目运行中,process.cwd() 在项目配置文件和测试中默认返回 Vitest 启动时的目录。项目的 root 属性控制 Vitest 在哪里查找文件,但不会改变进程的工作目录。Vite 插件可以从解析后的 Vite 配置中的 root 属性读取项目根目录。

如果测试需要 process.cwd() 指向项目目录,请使用 forks 池和每个项目特有的 setupFiles 文件:

packages/lib1/vitest.config.ts
ts
import { defineProject } from 'vitest/config'

export default defineProject({
test: {
pool: 'forks',
setupFiles: ['./setup.chdir.ts'],
},
})

packages/lib1/setup.chdir.ts
ts
import { fileURLToPath } from 'node:url'

process.chdir(fileURLToPath(new URL('.', import.meta.url)))

这会在配置加载后改变测试 worker 中的工作目录。threads 池无法使用 process.chdir()。

自定义包条件未被解析

如果你在 package.json 的 exports 或子路径导入中使用了自定义条件,你可能会发现 Vitest 默认不会尊重这些条件。例如,如果你的 package.json 包含以下内容:

json
{
"exports": {
".": {
"custom": "./lib/custom.js",
"import": "./lib/index.js"
}
},
"imports": {
"#internal": {
"custom": "./src/internal.js",
"default": "./lib/internal.js"
}
}
}

默认情况下,Vitest 仅使用 import 和 default 条件。要使 Vitest 尊重自定义条件,你需要在 Vitest 配置中设置 ssr.resolve.conditions:

vitest.config.js
ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
ssr: {
resolve: {
conditions: ['custom', 'import', 'default'],
},
},
})

为什么是 ssr.resolve.conditions 而不是 resolve.conditions?

Vitest 遵循 Vite 的配置约定:resolve.conditions 适用于 Vite 的客户端环境,对应 Vitest 的浏览器模式、jsdom、happy-dom 或带有 viteEnvironment: 'client' 的自定义环境。ssr.resolve.conditions 适用于 Vite 的 SSR 环境,对应 Vitest 的 node 环境或带有 viteEnvironment: 'ssr' 的自定义环境。由于 Vitest 默认使用 node 环境(即 viteEnvironment: 'ssr'),模块解析使用的是 ssr.resolve.conditions。这适用于包的 exports 和子路径导入。你可以在环境(environment)部分了解更多关于 Vite 环境和 Vitest 环境的信息。

段错误 (Segfaults) 和原生代码错误

在 pool: 'threads' 中运行原生 NodeJS 模块可能会遇到来自原生代码的晦涩错误。

Segmentation fault (core dumped)
thread '<unnamed>' panicked at 'assertion failed
Abort trap: 6
internal error: entered unreachable code

在这些情况下,原生模块很可能未构建为多线程安全。作为变通方法,你可以切换到 pool: 'forks',它会使用多个 node:child_process 而不是多个 node:worker_threads 来运行测试用例。

vitest.config.js
CLIs
import { defineConfig } from 'vitest/config'

export default defineConfig({
test: {
pool: 'forks',
},
})
bash
vitest --pool=forks

Worker 线程中时区不生效

在 setup 文件或测试中设置 process.env.TZ,或通过 env 设置 TZ,在 pool: 'threads' 和 pool: 'vmThreads' 中对 Date 没有影响。Node.js 仅在主要线程设置 TZ 时才应用时区。Worker 线程可以看到 process.env 的新值,但保留主进程的时间区域。

ts
process.env.TZ = 'Asia/Tokyo'
new Date('2026-01-01T00:00:00Z').getHours() // 在 forks 中为 9,在 threads 中保持不变

请在 Worker 启动前设置时区。使用 Shell、配置文件或 globalSetup;它们都在主进程中运行,因此在所有池中都有效。

CLI
vitest.config.js
globalSetup.js
bash
TZ=Asia/Tokyo vitest
ts
import { defineConfig } from 'vitest/config'

process.env.TZ = 'Asia/Tokyo'

export default defineConfig({})
ts
export default function () {
process.env.TZ = 'Asia/Tokyo'
}

如果测试在运行时需要不同的时区,请使用 pool: 'forks' 或 pool: 'vmForks'(每个 worker 是独立进程),或者向 Intl.DateTimeFormat 传递 timeZone 选项而不是更改 TZ。

未处理的 Promise 拒绝

当一个 Promise 被拒绝但在微任务队列清空前没有附加 .catch() 处理器或 await 时,会发生此错误。这种行为源自 JavaScript 本身,并非 Vitest 特有。详情请参阅 Node.js 文档。常见原因是调用异步函数但未 await 它:

ts
async function fetchUser(id) {
const res = await fetch(`/api/users/${id}`)
if (!res.ok) {
throw new Error(`User ${id} not found`)
}
return res.json()
}

test('fetches user', async () => {
fetchUser(123)
})

由于没有 await fetchUser(),其拒绝没有处理器,Vitest 会报告:Unhandled Rejection: Error: User 123 not found

修复方法

await Promise 以便 Vitest 可以捕获错误:

ts
test('fetches user', async () => {
await fetchUser(123)
})

如果你预期调用会抛出异常,请使用 expect().rejects:

ts
test('rejects for missing user', async () => {
await expect(fetchUser(123)).rejects.toThrow('User 123 not found')
})

包在 Vitest 中加载失败但在应用中正常

某些包在应用构建中有效,但在 Vitest 中失败,因为它们只有在打包器重写或解析后才有效。当 Vitest 将依赖项外部化时,Node.js 直接加载它,因此 Node 的 ESM 和包规则生效。请参阅 Node.js 关于 ECMAScript 模块和包的文档以了解精确规则。常见示例包括以下包:

  • 在 .js 文件中包含 ESM 语法但没有 "type": "module"
  • 在 ESM 文件中使用无扩展名的相对导入
  • 具有错误的 exports、imports、main 或 module 入口
  • 混合 CommonJS 和 ESM 入口点的方式仅在打包后有效
  • 导入 CSS 或其他非 JavaScript 文件,且预期由打包器处理

你可能会看到此类错误:Cannot find module './relative-path' imported from ...、Unexpected token 'export'、Cannot use import statement outside a module、Module ... seems to be an ES Module but shipped in a CommonJS package. 或 Unknown file extension ".css"

在可能的情况下,修复包以便 Node.js 可以直接加载:为 ESM .js 文件添加 "type": "module",使用 .mjs,在 ESM 导入中包含明确的文件扩展名,并确保 exports 指向 Node.js 可以加载的文件。如果你无法修复包本身,请将其内联(inline),让 Vite 处理它,而不是作为外部依赖传递给 Node.js。内联导致无效包的所有依赖链。如果你的源代码导入 wrapper-package,而 wrapper-package 导入 broken-package,请内联这两个包:

vitest.config.js
ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
test: {
server: {
deps: {
inline: ['wrapper-package', 'broken-package'],
},
},
},
})

你也可以使用 Vite 的 ssr.resolve.noExternal 达到相同目的。Vitest 将 ssr.resolve.noExternal 合并到 server.deps.inline 中,因此当依赖项在 SSR 构建中也需要由 Vite 打包时,这很有用:

vitest.config.js
ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
ssr: {
resolve: {
noExternal: ['wrapper-package', 'broken-package'],
},
},
})

CommonJS 源代码不支持完全

Vitest 是 ESM 优先的。默认情况下,源文件在 Vite 的模块运行器中运行,它提供 require、module 和 exports 等 CommonJS 变量以保持兼容性,但不完全复现 Node.js 的 CommonJS 语义。require() 调用始终直接使用 Node.js 并离开模块运行器。结果是:

  • Vite 插件、别名、转换和模块模拟不适用于通过 require 加载的文件
  • 不支持 require TypeScript 或其他 Node.js 无法执行的文件
  • 导入和 require 同一文件可能导致该文件被评估两次,这可能破坏单例状态、对象身份或 instanceof 检查

如果你的项目使用 CommonJS 且不需要 Vite 转换,请设置 experimental.viteModuleRunner 为 false,以便整个模块图由原生运行时加载:

vitest.config.ts
ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
test: {
experimental: {
viteModuleRunner: false,
},
},
})

如果应用程序使用 ESM 源但导入了同一 monorepo 中的 CommonJS 包,你可以改为使用 server.deps.external 将完整的 CommonJS 包外部化。这使其入口点和内部 require() 调用保持在同一个原生模块缓存中。例如:

vitest.config.ts
ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
test: {
server: {
deps: {
external: [/\/packages\/legacy-cjs\//],
},
},
},
})

评论 (0)