实用提示:Node 原生测试运行器与 Jest、Vitest 的对比:一个测试套件,三种选择
《实用笔记》操作指南:Node 原生测试运行器与 Jest、Vitest 的对比——同一套测试方案,三种工具:契约测试、检查测试,以及适用于采用该模式的团队的即插即用代码模块。
可将此内容视为《Node原生测试运行器与Jest、Vitest对比:一个测试套件,三种运行器,实时性能数据》一文的面向操作员的优化版本:清晰的阶段划分、有序的代码执行顺序,以及便于交接时参考的恢复说明。在扩大范围之前,应先将“概览”阶段视为可量化的基准,记录一份理想的测试结果、一个失败案例以及回滚说明。应将此阶段视为输入与经过验证的输出之间的契约,为相关成果命名、明确成功标准,绝不允许出现无声无息的半完成状态。
坦白说,这三种运行器
在修改代码之前,应先明确三个运行阶段的相关参数:输入内容、该步骤的负责人以及退出标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。除了功能结果外,还需记录执行时间以及令牌或查询成本。提前了解这些成本信息,就能避免在从演示环境切换到共享环境时出现意外费用。状态应与负责修改的组件放在一起管理;如果将所有数据都存放在全局存储中,就很难发现与时间相关的错误。
1. node:test:虽乏趣味但确实有效的选择
在针对1节点的测试阶段,应在修改代码之前明确输入参数、该步骤的负责人以及终止标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 配置信息应置于应用程序代码之外。环境文件、密钥存储以及功能标志应集中存放于一个位置,这样操作人员无需查看整个系统结构即可进行审计。 状态信息应与负责执行变更的组件放在一起。将所有数据都存放在全局存储中会使得时序错误更难被发现。
// node-test/test/string-utils.test.js
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { slugify } from '../../src/string-utils.js';
describe('slugify', () => {
it('converts a basic sentence', () => {
assert.equal(slugify('Hello World'), 'hello-world');
});
it('strips diacritics', () => {
assert.equal(slugify('Café résumé'), 'cafe-resume');
});
it('throws TypeError on non-string input', () => {
assert.throws(() => slugify(123), TypeError);
});
});
# Run it, no install step
node --test node-test/test/*.test.js
# Watch mode
node --test --watch node-test/test/*.test.js
# Coverage (still experimental, but the numbers are real V8 counts)
node --test --experimental-test-coverage \
--test-coverage-include='src/**/*.js' \
node-test/test/*.test.js
2. 你一直在使用的默认测试工具 Jest
在 2 Jest 的默认阶段中,修改代码之前需先定义输入参数、该步骤的负责人以及退出标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 需同时记录正常流程与异常恢复路径。重试机制、人工审核环节以及错误处理都是产品本身的组成部分,而非后续需要补充的功能。 状态应与负责数据变更的组件放在一起管理。将所有数据都放到全局存储中会使得时序相关的问题更难被发现。 在 2 Jest 的默认阶段中,修改代码之前需先定义输入参数、该步骤的负责人以及退出标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 应将此阶段视为输入与验证后输出之间的契约。为相关产物命名,明确成功判定标准,杜绝无声的半完成状态。
// jest/test/string-utils.test.js
import { slugify } from '../../src/string-utils.js';
describe('slugify', () => {
it('converts a basic sentence', () => {
expect(slugify('Hello World')).toBe('hello-world');
});
it('strips diacritics', () => {
expect(slugify('Café résumé')).toBe('cafe-resume');
});
it('throws TypeError on non-string input', () => {
expect(() => slugify(123)).toThrow(TypeError);
});
});
// jest/jest.config.js, minimal. Run it with NODE_OPTIONS=--experimental-vm-modules
export default {
rootDir: '.',
testMatch: ['<rootDir>/test/**/*.test.js'],
testEnvironment: 'node',
verbose: true,
};
# Run (the flag is required for native ESM)
NODE_OPTIONS=--experimental-vm-modules npx jest --config jest/jest.config.js
# Watch
NODE_OPTIONS=--experimental-vm-modules npx jest --config jest/jest.config.js --watch
3. Vitest:新的默认选择,但有一个权衡
在采用全新的Vitest阶段时,首先需明确合约的细节:所需的输入参数、成功信号以及部分失败时的处理方式。这样的清单能确保后续的代码修改保持一致性。 在功能结果旁记录执行时间以及代币或查询成本。提前了解成本情况,可避免在从演示环境过渡到共享环境时出现意外费用。 应将各种效果视为与外部世界的同步机制,而非渲染过程中衍生值的替代品。
// vitest/test/string-utils.test.js
import { describe, it, expect } from 'vitest';
import { slugify } from '../../src/string-utils.js';
describe('slugify', () => {
it('converts a basic sentence', () => {
expect(slugify('Hello World')).toBe('hello-world');
});
it('strips diacritics', () => {
expect(slugify('Café résumé')).toBe('cafe-resume');
});
it('throws TypeError on non-string input', () => {
expect(() => slugify(123)).toThrow(TypeError);
});
});
// vitest/vitest.config.mjs
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
include: ['test/**/*.test.js'],
environment: 'node',
coverage: { provider: 'v8', include: ['../src/**/*.js'] },
},
});
# Run
cd vitest && npx vitest run
# Watch (this is the killer feature)
cd vitest && npx vitest
# Coverage
cd vitest && npx vitest run --coverage
该方法论,让你有理由质疑
在按照该方法论分阶段进行开发时,首先写下契约内容:所需的输入参数、成功标志,以及部分失败时的处理方式。这样的检查清单能确保后续的代码修改保持一致性。 将配置信息与应用程序代码分开存放。环境文件、密钥存储以及功能开关应集中于一个位置,这样操作人员无需查看整个系统结构即可进行审计。 应将效果处理视为与外部世界的同步机制,而非替代渲染过程中生成的衍生值。
# Reproduce on your own machine
git clone https://github.com/manisuec/techinsights-tutorials
cd techinsights-tutorials/nodejs-test-runner
npm install
bash shared/bench.sh 5 # cold runs
WARM=1 bash shared/bench.sh 5 # warm runs
真实数值
在处理“实数”阶段时,首先需明确契约内容:所需输入、成功信号以及部分失败时的处理方式。这份清单能确保后续的代码修改保持一致性。 同时记录正常流程与恢复流程。重试机制、人工审核环节以及死信处理都是产品本身的组成部分,而非后续的优化工作。 应将各种效应视为与外部世界的同步机制,而非渲染过程中衍生值的替代品。 在处理“实数”阶段时,首先需明确契约内容:所需输入、成功信号以及部分失败时的处理方式。这份清单能确保后续的代码修改保持一致性。 要把这一阶段视为输入与经过验证的输出之间的契约。为相关成果命名,定义成功检测标准,并拒绝默许部分完成的情况。
=== node:test (Node v24.14.0) [cold] ===
run 1: 91 ms
run 2: 93 ms
run 3: 89 ms
run 4: 90 ms
run 5: 90 ms
median: 90 ms
=== Jest (30.4.1) [cold] ===
run 1: 817 ms
run 2: 689 ms
run 3: 718 ms
run 4: 711 ms
run 5: 685 ms
median: 711 ms
=== Vitest (4.1.11) [cold] ===
run 1: 548 ms
run 2: 648 ms
run 3: 534 ms
run 4: 531 ms
run 5: 535 ms
median: 535 ms
功能矩阵,2026年,上次查看时间
将“特征矩阵2026”最后阶段视为可测量的表面来处理时,其效果最佳。在扩大范围之前,先记录一份理想的测试用例、一个故障案例以及回滚说明。在功能结果旁同时记录执行时间以及令牌或查询成本。提前了解成本情况,可避免在从演示环境过渡到共享环境时出现意外账单。尽量保持渲染工作的成本较低,只有在经过测量后才将高成本的推导操作放入记忆化机制中。过早使用记忆化可能会掩盖过时的属性错误。
何时选择哪种方法
将选择哪个阶段视为可测量的指标,这样就能判断何时采用最佳方案。在扩大范围之前,先记录一个成功的案例、一个失败案例以及回滚说明。 将配置与应用程序代码分开。环境文件、密钥存储和功能标志应集中存放,以便操作人员无需查看整个结构即可进行审计。 尽量降低渲染成本,只有在经过测量后,才将耗时的计算操作放在记忆化机制之后处理。过早使用记忆化可能会掩盖过时属性带来的错误。
30分钟内从 Jest 迁移到 Vitest
将从 Jest 迁移到测试阶段的流程视为可度量的工作面时,效果最佳。在扩大范围之前,先记录一份理想的测试用例、一个失败案例以及回滚说明。 同时记录正常流程与异常恢复流程。重试机制、人工审核环节以及死信处理都是产品功能的一部分,而非后续需要补充的内容。 尽量降低渲染成本,只有在经过评估后才能将耗时的计算操作放在记忆化机制之后处理。过早使用记忆化可能会掩盖属性过时的问题。 将从 Jest 迁移到测试阶段的流程视为可度量的工作面时,效果最佳。在扩大范围之前,先记录一份理想的测试用例、一个失败案例以及回滚说明。 把这一阶段视为输入与经过验证的输出之间的契约。为相关成果命名,明确成功标准,杜绝无声的半完成状态。
# 1. Install
npm install -D vitest
# 2. Add a config (or piggyback on vite.config.ts if you have one)
# vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true, // makes describe/it/expect global
environment: 'jsdom', // or 'node' for backend
setupFiles: ['./tests/setup.ts'],
},
});
# 3. Swap imports, find/replace in your editor
# jest.mock → vi.mock
# jest.fn → vi.fn
# jest.spyOn → vi.spyOn
# require('@jest/globals') → require('vitest')
# jest.useFakeTimers() → vi.useFakeTimers()
# 4. Swap npm scripts
# "test": "jest" → "test": "vitest run"
# "test:watch": "jest --watch" → "test:watch": "vitest"
# 5. If you used babel-jest, delete it and babel.config.js
# 6. Run. Fix the 3-5 things that fail. Drink coffee.
不应采取的做法
对于那些不适合直接上线的功能,应在修改代码之前明确输入参数、该步骤的负责人以及结束标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。除了功能结果外,还需记录执行时间以及令牌或查询成本。提前了解这些成本信息,可避免在从演示环境过渡到共享环境时出现意外费用。状态应与负责处理数据变更的组件放在一起;如果将所有数据都存放在全局存储中,就很难发现与时间相关的错误。
结论
在判定阶段,应在修改代码之前明确输入参数、该步骤的负责人以及退出标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 配置信息应置于应用程序代码之外。环境文件、密钥存储以及功能开关应集中存放于一个位置,这样操作人员无需查看整个系统结构即可进行审计。 状态信息应与负责处理数据变更的组件放在一起。将所有数据都存放在全局存储中会使得时序错误更难被发现。
附录:如何理解本文中的数据
在附录中,应在修改代码之前明确如何读取阶段状态、定义输入参数、确定该步骤的负责人以及设定退出条件。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 需同时记录正常流程与异常恢复路径。重试机制、人工审核环节以及错误处理都是产品本身的组成部分,而非后续需要补充的功能。 状态信息应与负责数据变更的组件放在一起。将所有数据都存放在全局存储中会使得时序相关错误更难被发现。 在附录中,应在修改代码之前明确如何读取阶段状态、定义输入参数、确定该步骤的负责人以及设定退出条件。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 应将此阶段视为输入与经过验证的输出之间的契约。为相关产物命名,明确成功判定标准,杜绝无声的半完成状态。
操作检查清单
在处理操作检查清单阶段时,首先写下相关契约:所需的输入参数、成功标志,以及部分失败时的处理方式。这样的检查清单能确保后续的代码修改保持一致性。
优先选择小型、可测试的单元,而非冗长的脚本。当某个步骤失败时,故障应指向单一责任模块,而非复杂的流程链。
应将效果视为与外部世界的同步机制,而非渲染过程中衍生值的替代品。
锁定依赖项的版本,并记录用于演示的图像摘要。可重复性比团队内部的知识更重要。
将此阶段视为输入与经过验证的输出之间的契约。为相关产物命名,明确成功标准,拒绝默许的不完整完成状态。
应将效果视为与外部世界的同步,而非渲染过程中派生值的替代品。
在提升栈版本之前,先冻结各版本状态,为关键路径记录黄金转录副本,并明确回滚步骤。共享环境需要设置速率限制、租户验证机制,以及明确的密钥轮换负责人。与其追求巧妙的单次演示,不如注重扎实的可靠性。
b78b143fb7a9版本的批注:不要将提供方密钥放入代码仓库,为每个会话设置令牌使用上限,并将转录副本存储在评估用固定文件旁,以便后续模型更换时保持数据可比性。