SuperAPI AI 工具集

javascript-testing-expert

测试与质量

查看项目

更新于 10-15 16:58

技能介绍

用途

该技能用于指导 JavaScript 测试的编写,目标是产出高质量测试,使其能够:发现难以检测的缺陷、记录代码的使用方式、避免回归、挑战代码本身。其范围明确限定为测试函数与组件,不涉及黑盒端到端测试。

推荐工具

推荐使用 vitest、fast-check、@fast-check/vitest、@testing-library/*、@vitest/browser-playwright、msw,并以 devDependencies 形式安装。技能建议在缺失相关工具时进行安装,尤其推荐 @fast-check/vitest 与浏览器测试相关工具,同时要求根据缺失的工具自行调整。

文件与代码布局

新增测试时应模仿项目已有的测试结构;每个代码文件对应一个测试文件。若项目没有既有测试结构,优先使用 .spec.ts 扩展名(如 fileName.ts → fileName.spec.ts)并与源文件同目录放置。使用 it 时应放在 describe 内,优先使用 it 而非 test;describe 以被测函数命名,每个被测函数使用独立的 describe。it 的命名以 "should" 开头,名称应清晰、尽量简洁,可被理解为隐式以 "it" 为前缀的句子。先写简单且具文档性质的测试,再写寻找边界情况的高级测试,但不要显式区分简单与高级测试,只需按正确顺序排列。文件专属的辅助函数放在所有 describe 之后,并以注释 // Helpers 标明该文件专属辅助函数的开始。

核心准则

遵循 AAA 模式(Arrange、Act、Assert)并在测试中显式体现。保持测试聚焦,尽量只断言一个精确方面;保持测试简单,避免在测试或其辅助函数中使用复杂逻辑;不要测试内部细节。优先使用 stub 而非 mock:前者提供替代实现,后者用于断言调用是否发生;因为断言调用次数往往并非函数使用者的关键需求,而只是内部细节。不要依赖网络调用,应使用 msw 进行 stub。若测试涉及 mock、spy 或修改全局变量,应在 beforeEach 中重置全局变量与 mock;使用 vitest 时也可检查配置中是否启用了 mockReset、unstubEnvs、unstubGlobals 标志,若启用则默认已重置全局变量。文档性质的测试优先使用真实数据,例如构建用户实例时使用真实姓名。不要过度使用快照测试,仅在“快照中预期看到什么”清晰时才使用快照;快照测试容易捕获过多细节,导致后续更新困难、读者不清楚真正被测内容。当形状与结构重要时(组件层级、属性、输出结构的非回归)优先使用快照;当最终渲染重要时(视觉样式、布局)优先使用截图。当被测代码需要过多参数和/或过多 mock/stub(超过 10 个)时,应提醒开发者,因为难以测试的代码往往是代码坏味道,指向需要修改的 API,建议遵循单一职责原则(SRP)。通过将重复逻辑提取到辅助函数中,使测试更短、更易读;将共享逻辑归入名称清晰明确的函数,辅助函数也遵循 SRP,例如避免带有大量可选参数、做多件事的函数。不要编写被所有测试在 act 部分复用的大型 prepare 函数,而应让名称更清晰,必要时拆分为多个函数。确保测试在移除“使其通过”的东西后会失败,例如测试声称“should do X when Y”时,确保没有 Y 时测试会失败。避免在测试中为未使用字段硬编码值。

属性测试指南

版本兼容性:从 fast-check v5 起,同步与异步谓词均使用 fc.property;在 fast-check v4 中,同步谓词使用 fc.property,异步谓词使用 fc.asyncProperty。直接使用 fast-check 时,无论 v4 还是 v5+,即使谓词是同步的,也应始终 await fc.assert(...),并将外层测试回调设为 async,以便属性失败由测试运行器报告。使用 @fast-check/vitest 的 g 时,应将 arbitrary 函数(如 fc.string、fc.date)及其参数作为独立参数传给 g,而不是传入调用结果。正确:g(fc.string)、g(fc.date, { min: new Date('2010-01-01') });错误:g(fc.string())、g(fc.date({ min: new Date('2010-01-01') }))。不要直接生成输入,否则可能最终在测试中重写被测代码;应构造值以构建已知预期结果的输入。不要详细断言返回值,很多情况下没有足够细节断言完整值;应断言返回值的某些方面与特征。若某 arbitrary 的 maxLength 不是算法需求,绝不要指定;若认为算法在大输入上耗时很长,优先指定 size: '-1'(fast-check 默认生成最多 10 项,仅在明确需要时使用 size)。若某约束不是 arbitrary 的需求,绝不要指定,尽量使用默认值,例如算法应接受任意整数时就直接请求整数而不指定 min 和 max。避免过度使用 .filter 和 fc.pre,因为它们会丢弃生成值从而减慢生成速度;优先使用 arbitrary 提供的选项直接生成有效值,例如用 fc.string({ minLength: 2 }) 代替 fc.string().filter(s => s.length >= 2),用 fc.integer({ min: 1 }) 代替 fc.integer().filter(n => n >= 1),或用 fc.nat() 代替 fc.integer().filter(n => n >= 0)。当 map 技巧可避免过滤时优先使用 map 而非 filter,例如用 fc.nat().map(n => n * 2) 生成偶数,用 fc.tuple(fc.string(), fc.string()).map(([start, end]) => start + 'A' + end) 生成总含 'A' 的字符串。当谓词中的整数计算存在溢出风险时(如对生成值执行 pow、multiply),优先使用 bigint 类型而非 number 类型。

一些经典属性包括:与输入无关的特征,例如对任意浮点数 d,Math.floor(d) 是整数;对任意整数 n,Math.abs(n) ≥ 0。由输入推导的特征,例如对任意整数 a 和 b,a 与 b 的平均值介于 a 和 b 之间;对任意 n,n 的质因数分解中所有数的乘积等于 n;对任意数据数组,sorted(data) 与 data 包含相同元素;对任意整数 n1、n2 且 n1 != n2,romanString(n1) != romanString(n2);对任意浮点数 d,Math.floor(d) 是整数且满足 d-1 ≤ Math.floor(d) ≤ d。具有有用特征的受限输入集,例如对任意无重复的数组 data,去重结果就是 data 本身;对任意字符串 a、b、c,a、b、c 的拼接总是包含 b;对任意质数 p,其质因数分解就是自身。函数组合的特征,例如对文件先 zip 再 unzip 应得到原文件;lcm(a,b) 乘以 gcd(a,b) 必须等于 a 乘以 b。与更简单实现的比较,例如二分查找中 c 是否包含于有序数组 data,等价于线性查找中 c 是否包含于 data。

竞态条件指南

当算法接受异步函数作为输入时,应编写检查竞态条件、并利用解析顺序的测试——fast-check 会自动处理。应利用 fast-check 及其 fc.scheduler() arbitrary 测试依赖异步函数的异步代码。资料给出了将基于 vi.fn() 的“按调用顺序解析”测试改写为使用 fc.scheduler() 的示例(fast-check v5+ 使用 fc.property,v4 使用 fc.asyncProperty)。

faker 用户建议

若使用 faker 伪造数据,建议通过资料提供的代码片段将任何伪造数据生成接入 fast-check。该片段定义了 FakerBuilder 类与 fakerToArb 函数,并给出在 fc.assert(fc.property(...)) 中的使用示例。

fast-check 与 @fast-check/vitest 的等价写法

资料给出三组等价示例:使用 @fast-check/vitest 的 it 与 g;使用 it.prop([...arbitraries]);以及异步谓词场景(fast-check v5+ 使用 fc.property,v4 使用 fc.asyncProperty,并始终 await fc.assert)。

适用场景与限制

适用于 JavaScript 函数与组件的测试编写,尤其是希望借助属性测试发现边界情况、借助文档性质测试说明用法、借助竞态条件测试验证异步逻辑的场景。限制在于:不适用于黑盒端到端测试;属性测试与示例测试互补,不应试图用属性测试覆盖算法 100% 的情况;不要测试零复杂度的琐碎组件逻辑;不要测试内部细节。