close

从 Vitest 迁移

如果你正在使用 Rstack 工具链(Rsbuild / Rslib / Rspack 等),迁移到 Rstest 可以带来更一致的开发体验。

使用 Agent Skills

如果你在使用支持 Skills 的 Coding Agent,可以安装 migrate-to-rstest 技能来辅助完成从 Vitest 到 Rstest 的迁移。

npx skills add rstackjs/agent-skills --skill migrate-to-rstest

安装后,让 Coding Agent 协助完成升级即可。

安装依赖

首先,你需要安装 Rstest 依赖。

npm
yarn
pnpm
bun
deno
npm add @rstest/core -D

接下来,更新 package.json 中的测试脚本,使用 rstest 替代 vitest。例如:

"scripts": {
-  "test": "vitest run" // 或 "vitest --run"
+  "test": "rstest"
}

rstest 没有 --run 参数。直接运行 rstest 就会执行一次测试并退出;如果你想使用 watch 模式,可以加上 --watch

"scripts": {
-  "test": "vitest"
+  "test": "rstest --watch"
}

CLI 参数映射

Vitest 的一部分 CLI 参数可以直接映射到 Rstest,另一部分则需要调整写法。迁移时,最常遇到的差异可以参考下表:

Vitest CLI 参数Rstest 对应写法说明
vitest run / vitest --runrstest没有 --run 参数 —— 运行 rstest 默认就是执行一次。
vitest / vitest watch / vitest --watchrstest --watchrstest watchRstest 不会自动进入 watch 模式,需要显式加 --watch
vitest --coveragerstest --coverage还需安装 @rstest/coverage-istanbul(详见 coverage 配置行)。
vitest --environment=jsdomrstest --testEnvironment jsdom
vitest --reporter=verboserstest --reporter verbose
vitest --globalsrstest --globals
vitest -t <pattern> / --testNamePatternrstest -t <pattern> / --testNamePattern
vitest -u / --updaterstest -u / --update
vitest -c <path> / --configrstest -c <path> / --config
vitest --project <name>rstest --project <name>

配置迁移

将你的 Vitest 配置文件(例如 vite.config.tsvitest.config.ts)迁移为 rstest.config.ts

rstest.config.ts
import { defineConfig } from '@rstest/core';

export default defineConfig({
  // 根据下方映射表,从 vitest.config.ts 中逐字段迁移到这里。
});

Helper 映射

Vitest 配置文件中使用的 helper 可以对应到 @rstest/core 导出的同类方法:

Vitest(vitest/configRstest(@rstest/core说明
defineConfigdefineConfig
defineProjectdefineProjectprojects 数组内使用对象形式时,优先使用 defineInlineProject —— 它要求显式声明 name
defineWorkspace移除没有独立的 workspace helper。直接在 defineConfigprojects 字段内声明各 project —— 详见 Projects
mergeConfigmergeRstestConfig会进行 deep merge 并正确处理函数类型字段。在 projects 数组内组合单个 project 配置时,使用 mergeProjectConfig

Vitest 配置映射

迁移配置时,重点关注这两点:

  • 移除 test 字段,将其内部配置提升到顶层。
  • 一些字段名的调整(例如 test.environmenttestEnvironment)。

请遍历 test 下的每一个字段,对照下表进行提升、重命名或删除。表中未列出的字段未必能 1:1 映射,直接删除前请先对照 Rstest 配置参考 确认。

Vitest(test 下)Rstest(顶层)说明
environmenttestEnvironmenttest.environmentOptions 合并到对象形式:testEnvironment: { name: 'jsdom', options: { ... } }。不支持自定义 environment 包。
include / excludeinclude / exclude
includeSourceincludeSource
setupFilessetupFiles
globalSetupglobalSetupRstest 调用 setup 时不传参数 —— 如果你的 Vitest setup 读取了 TestProject 参数(如 provideonTestsRerun),迁移时需重写。Vitest 的 provide / inject 没有直接等价形式 —— 在 setup 里修改 process.env(Rstest 会在 setup 结束后快照并注入每个 worker),或使用 env 配置字段传递静态值。
globalsglobals
namename
rootroot
envenv
aliasresolve.aliasRstest 下不是 test.* 字段 —— 迁到顶层 resolve.alias
passWithNoTestspassWithNoTests
isolateisolate
testTimeout / hookTimeouttestTimeout / hookTimeout
teardownTimeout移除没有对等字段 —— Vitest 的 teardownTimeout 是 shutdown 等待超时,与 Rstest 的 hookTimeout(生命周期 hook)无关。
slowTestThresholdslowTestThreshold
maxConcurrencymaxConcurrency
retryretry
bailbail
clearMocksclearMocks
mockResetresetMocks
restoreMocksrestoreMocks
poolOptions.forks.maxForkspool.maxWorkersRstest 把 poolOptions 摊平到顶层 poolpoolOptions.forks.maxForkspool.maxWorkersminForkspool.minWorkersexecArgvpool.execArgv。只支持 forks pool —— pool: 'threads' | 'vmThreads' | 'vmForks' 这些配置会被丢弃(行为回退到 forks)。Vitest 4 的顶层 test.maxWorkers / test.minWorkers 也映射到 pool.maxWorkers / pool.minWorkers
coveragecoverage仅支持 provider: 'istanbul' —— 把 dev 依赖从 @vitest/coverage-v8 换成 @rstest/coverage-istanbul(去掉 'v8' / 'custom')。把 coverage.reporter 改为 coverage.reporters(单数写法会被静默忽略)。下列子字段 1:1 对应:includeexcludereportsDirectorythresholds。V8 专用字段(allskipFullthresholdAutoUpdateprocessingConcurrencycustomProviderModulewatermarksignoreClassMethods 等)没有对等字段。
reportersreportersVitest 独有(需替换或丢弃):taptap-flathtmltreehanging-process。字符串必须是内建 reporter 名称;第三方 reporter 需 import 类并传入实例。
outputFilereporter options没有顶层字段。junit / json 用 reporter 元组传 outputPath['junit', { outputPath: '...' }]blob{ outputDir: '...' };其他 reporter 不接受输出路径。对象形 { junit: 'a.xml', json: 'a.json' } 展开为每个 reporter 一条 tuple。
snapshotFormatsnapshotFormat
resolveSnapshotPathresolveSnapshotPathRstest 的 callback 签名是 (testPath, snapExtension) => string,没有 Vitest 3+ 的第三个 context 参数。
snapshotSerializersexpect.addSnapshotSerializer没有配置字段。在 setupFiles 模块里 import 每个 serializer,并调用 expect.addSnapshotSerializer(serializer)
projectsprojects
logHeapUsagelogHeapUsage
includeTaskLocationincludeTaskLocation
printConsoleTraceprintConsoleTrace
unstubGlobalsunstubGlobals
unstubEnvsunstubEnvs
chaiConfigchaiConfig

编译配置

Rstest 使用 Rsbuild 作为默认测试编译工具,而不是 Vite。你可以在 Build Configurations 查看全部编译配置项。

大部分项目中,主要的编译侧变化如下:

  • 使用 source.define 替代 define
  • 使用 output.externals 替代 ssr.external
  • 使用 Rsbuild 插件替代 Vite 插件。
import { defineConfig } from '@rstest/core';
- import react from '@vitejs/plugin-react'
+ import { pluginReact } from '@rsbuild/plugin-react';

export default defineConfig({
-  plugins: [react()],
-  define: {
-    __DEV__: true,
-  },
+  plugins: [pluginReact()],
+  source: {
+    define: {
+      __DEV__: true,
+    },
+  },
});

如果你使用的是 Rslib 或 Rsbuild,也可以直接复用对应配置:

  • Rslib 项目(存在 rslib.config.*)使用 @rstest/adapter-rslib,并在 extends 中配置 withRslibConfig()(参考 Rslib 集成文档)。
  • Rsbuild 项目(存在 rsbuild.config.*)使用 @rstest/adapter-rsbuild,并在 extends 中配置 withRsbuildConfig()(参考 Rsbuild 集成文档)。

更新测试 API

测试 API

Rstest 提供了与 Vitest 兼容的 API,已有的 Vitest 测试文件通常只需要极少改动。将 vitest 的导入替换为 @rstest/core,并把 vi / vitest 工具 API 替换为对应的 rs / rstest

- import { describe, expect, it, test, vi, type Mock } from 'vitest';
+ import { describe, expect, it, test, rs, type Mock } from '@rstest/core';
- vi.fn()
+ rs.fn()

- vi.mock('./foo')
+ rs.mock('./foo')

- vi.spyOn(console, 'error')
+ rs.spyOn(console, 'error')
- vitest.fn()
+ rs.fn()

完整工具 API 请参考 Rstest APIs

全局 API

当启用 globals: true 时,Vitest 会把 vivitest 挂在全局对象上。在 Rstest 中,建议按以下顺序映射:

  • vi.<api>rs.<api>
  • vitest.<api>rstest.<api>

rsrstest 是等价的全局别名,但迁移时按这个一一对应关系更容易阅读和排查。

- vi.fn()
+ rs.fn()

- vitest.spyOn(console, 'error')
+ rstest.spyOn(console, 'error')

@rstest/core 导入 API 时,统一使用 import style 的 rs.<api> 更一致,避免在同一文件里与 global style 混用。

Setup adapter

有些 setup adapter 是 Vitest 专用的。例如 @testing-library/jest-dom/vitest 面向 Vitest;在 Rstest 中通过 expect.extend 直接注册 matcher。

- import '@testing-library/jest-dom/vitest';
+ import * as jestDomMatchers from '@testing-library/jest-dom/matchers';
+ import { expect } from '@rstest/core';
+
+ expect.extend(jestDomMatchers);

路径解析

在某些 transform/runtime 模式下,new URL(..., import.meta.url) 可能会在 setup 或 helper 文件中失效。

如果你看到 Cannot find module './'Cannot find module '..' 这类路径错误,建议改用 Node 风格的 __dirname 路径解析:

- const root = fileURLToPath(new URL('../..', import.meta.url));
+ import { resolve } from 'node:path';
+ const root = resolve(__dirname, '../..');

自动模拟模块

在 Vitest 中,调用 vi.mock() 且只传模块路径时,会先尝试从对应 __mocks__ 目录加载手动 mock;如果没找到,再自动 mock 整个模块,把导出替换为空 mock 函数。

// Vitest
import { vi, test, expect } from 'vitest';
import { someFunction } from './module';

// 优先查找 __mocks__/module.js,然后自动 mock。
vi.mock('./module');

test('should be mocked', () => {
  expect(vi.isMockFunction(someFunction)).toBe(true);
  someFunction(); // 返回 undefined
});

Rstest 的行为不同。调用 rs.mock() 且只传模块路径时,只会查找 __mocks__,若未找到会报错。启用自动 mock 需要显式传入 { mock: true }

// Rstest
import { rs, test, expect } from '@rstest/core';
import { someFunction } from './module';

- // 优先查找 __mocks__/module.js,然后自动 mock。
- vi.mock('./module');
+ // 传入 { mock: true } 后会自动 mock 模块。
+ rs.mock('./module', { mock: true });

test('should be mocked', () => {
  expect(rs.isMockFunction(someFunction)).toBe(true);
  someFunction(); // 返回 undefined
});

Mock 异步模块

当你需要 mock 模块返回值时,Rstest 不支持返回异步函数。

作为替代,Rstest 提供了同步 importActual 能力,你可以通过静态 import 导入未 mock 的真实实现:

import * as apiActual from './api' with { rstest: 'importActual' };

// 部分 mock './api' 模块
rs.mock('./api', () => ({
  ...apiActual,
  fetchUser: rs.fn().mockResolvedValue({ id: 'mocked' }),
}));

mock factory 是 hoisted 执行的;依赖同模块中后初始化的变量会触发初始化顺序错误。共享值可放到 hoisted initializer(例如 rs.hoisted(...))中规避。

Snapshot

Vitest 和 Rstest 使用相同的 snapshot key 格式和 body 序列化方式。原有的 __snapshots__/*.snap 文件可以被 Rstest 原样读取;在 Vitest 下能通过的测试,到 Rstest 下也能通过,不需要重录。两者只有文件 header 行不同:

- // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+ // Rstest Snapshot v1

执行 rstest -u 会把 header 规整为 Rstest 形式,snapshot body 保持 byte-identical。