用途
该技能用于将 Cypress 仓库中 @packages/server 的单元测试规范从 mocha 迁移到 vitest。迁移范围严格限定在 packages/server/test/unit 目录;集成测试与性能测试套件继续使用 mocha。技能的核心目标是保证迁移后的测试在 vitest 下运行,且测试所验证的内容与 mocha 原版完全一致。
主要能力
- 文件命名与运行器约定:
*_spec.ts通过test/scripts/run.js在 mocha 上运行;*.spec.ts通过packages/server/vitest.config.ts在 vitest 上运行。yarn test-unit会同时运行两者。 - 完整移植要求:使用
git mv foo_spec.ts foo.spec.ts重命名并重写文件,完成后原 mocha 文件必须不存在。不得留下半转换文件:要么原文件中的每个it都被移植,要么完全不触碰该文件。 - 测试辅助工具替换:新文件必须显式从 vitest 导入,不得使用
spec_helper、mockery、proxyquire、sinon、chai及其插件、snap-shot-it、mocked-env。nock、supertest、express、lodash、bluebird等若原文件用于实际工作,则保留。 - 禁止削弱测试:不得添加
.skip,不得删除或放宽断言,不得使用vitest -u让快照通过。若测试无法变绿,应保留失败状态并在 PR 中说明原因。 - 禁止触碰特定文件:不得修改
.circleci/、vitest.config.ts或cloud/environment_spec.ts(后者在四个 CI 工作流中被引用,需单独的人工 PR 迁移)。 - 网络限制:测试不得连接非回环地址,必须在沙箱主机上通过。
使用方式
内联 spec_helper 钩子
spec_helper.js 不会随迁移移植。由于 mocha 规范可能隐式依赖其他规范安装的根钩子,而 vitest 中每个文件独立运行,因此需要阅读 spec_helper.js,仅内联当前文件实际依赖的钩子。技能给出了常见钩子的内联对照表,例如:
setCtx(makeDataContext({}))在before中,getCtx()._reset()与clearCtx()在after中,仅当 SUT 调用getCtx()时使用。nock.disableNetConnect()与nock.enableNetConnect(/localhost/)在before中,nock.cleanAll()与nock.enableNetConnect()在after中,任何使用 nock 的规范都需要。cache.remove()在每个测试前,用于触碰lib/cache的规范。GracefulExit.resetForTesting()用于启动服务器的规范,且需先设置globalThis.IS_TEST = true。process.env克隆与恢复,或使用vi.stubEnv与vi.unstubAllEnvs()。mockElectron(mockery)替换为vi.mock('electron', ...),仅当 SUT 的导入图触及 electron 时。sinon.restore()替换为vi.restoreAllMocks(),但需注意其会清除vi.fn().mockReturnValue(...)的行为。
Mock 验证与路径匹配
每个 mock 都必须证明其确实拦截了目标模块。可临时在工厂函数中添加 throw 或调用计数,运行规范后移除。仅凭测试通过不能证明 mock 生效。vi.mock 的说明符必须是导入方书写的字符串(相对路径从当前规范位置解析),例如:
vi.mock('../../../../lib/cloud/network/fetch')
若路径解析到别处,mock 会被忽略,真实模块被加载。
CJS require 拦截
SUT 内部的裸 require() 永远不会被 vi.mock 拦截。需要像 mockery 那样填充 CJS 缓存:
const requireCjs = createRequire(import.meta.url)
const morganPath = requireCjs.resolve('morgan')
requireCjs.cache[morganPath] = { exports: morganMockFactory } as NodeModule
// afterAll / afterEach: delete requireCjs.cache[morganPath]
exports 值应恰好是 SUT 的 require() 返回的内容。
部分 mock Node 内置模块
必须同时覆盖 default 导出,否则使用 import fs from 'fs' 的 SUT 会读取到真实函数:
vi.mock('fs', async (importOriginal) => {
const actual = await importOriginal<typeof import('fs')>()
return { ...actual, createReadStream: stub, default: { ...actual, createReadStream: stub } }
})
断言语义转换
chai的deep.equal对应toStrictEqual,而非toEqual。toEqual会忽略值为undefined的键,导致断言过宽。sinon的calledWith是前缀匹配;vitest 的toHaveBeenCalledWith要求精确参数数量。需断言完整参数列表或按前导参数过滤mock.calls。sinon.stub().withArgs(x)返回过滤后的 stub;若原代码直接赋值该返回值,则过滤从未生效,stub 会响应所有调用。移植前需确认 mocha stub 的实际行为。vi.restoreAllMocks()会清除vi.fn().mockReturnValue(...)的实现;共享夹具应使用vi.fn(() => value)以在 restore 后保留实现。
Bluebird 与计时器
若 SUT 在 mock 返回值上链式调用 Bluebird 方法(如 .return()、.spread()、.tap()),mock 应返回 Bluebird.resolve(...) 而非原生 Promise。使用 vi.useFakeTimers() / vi.useRealTimers(),clock.tickAsync(n) 变为 await vi.advanceTimersByTimeAsync(n)。对于 nock 的 delayConnection,建议使用 vi.runAllTimersAsync() 分轮执行或小步推进时钟。
GraphQL 与 schema 重复构建
导入 lib/server-base、lib/routes 或 lib/makeDataContext 会在 worker 中第二次构建 GraphQL schema,graphql 会拒绝重复 realm。需要 mock @packages/data-context/graphql/makeGraphQLServer。若同时调用 makeDataContext,还需使用仅包含 SUT 触及成员的最小 DataContext 对象。
源码修改
仅允许明显可测性改进:添加保留行为的可选默认参数、导出已存在的函数、将内联表达式提取为同一文件中的命名纯函数。不允许改变控制流、改变调用方依赖的签名、每批触碰超过一个 lib/ 文件,或无法用一句话说明理由的修改。每次源码修改需在 PR 正文中单独标题说明。
适用场景
适用于 Cypress 仓库中 packages/server/test/unit 下 mocha 单元测试向 vitest 的迁移工作。需要开发者已经熟悉 sinon.stub 到 vi.fn、context 到 describe 等基础 API 映射。
限制
- 迁移范围仅限
packages/server/test/unit;集成与性能套件不迁移。 - 不得修改
.circleci/、vitest.config.ts或cloud/environment_spec.ts。 - 不得削弱测试;无法变绿的测试保留失败并在 PR 中说明。
- 测试不得连接非回环地址。
- 技能不提供收费、平台或效果相关信息。
验证
yarn workspace @packages/server test-unit-vitest -- test/unit/<dir>/foo.spec.ts
yarn workspace @packages/server test-unit # both runners, from a real terminal
yarn lint --scope @packages/server
yarn workspace @packages/server check-ts
转换后的规范至少运行两次。test-unit 需在真实终端运行,因为 util/tty_spec 依赖 process.stdout.getWindowSize,仅 TTY 存在。对比 mocha 原文件与 vitest 文件中 it( 的静态数量,二者必须一致。
代码风格
单引号、无分号、2 空格缩进、尾随逗号、return 前空行、类型导入使用 import type、无 console。尽量不写注释;必要时用一两行说明原因并描述当前代码,而非过去如何。