news 2026/10/24 1:01:02

fast-check 4.4.0 解析:原生 Map 与 Set 的生成器 fc.map / fc.set,以及 fc.dictionary 的完整 PropertyKey 支持

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
fast-check 4.4.0 解析:原生 Map 与 Set 的生成器 fc.map / fc.set,以及 fc.dictionary 的完整 PropertyKey 支持
  • 开发工具
  • 测试

【免费下载链接】fast-check

Property based testing framework for JavaScript (like QuickCheck) written in TypeScript

项目地址:https://gitcode.com/gh_mirrors/fa/fast-check
点击查看免费下载

本篇围绕 fast-check 4.4.0 版本的核心更新展开:新增的fc.map与fc.set两个 arbitrary、fc.dictionary对完整PropertyKey(字符串、数字、Symbol)键的支持。结合仓库中的源码实现与集成测试,你将理解这些生成器的约束参数如何工作、为何SameValueZero比较语义是它们与原生Map/Set保持一致的关键,以及底层“数组 + 映射器”架构如何支撑起完整的 shrink(收缩)能力。

该版本公告原文位于 4.4.0 发布公告,对应的完整变更清单可在 CHANGELOG 中查阅。

版本 4.4.0 带来了什么

4.4.0 是一次面向“集合类型”的功能发布,官方公告将其概括为三件事:

  1. 新增fc.maparbitrary:专门用于生成 JavaScript 原生Map实例;
  2. 新增fc.setarbitrary:专门用于生成原生Set实例;
  3. fc.dictionary支持完整的PropertyKey:此前fc.dictionary的键只能是字符串,而现在可以生成以字符串、数字或 Symbol 为键的对象属性。

官方公告的原话是:这两个原生集合现在是 fast-check 中的“一等公民”(first-class citizens),让依赖它们的代码更容易被属性测试覆盖。

从 入口文件 可以确认,map、set及其约束类型MapConstraints、SetConstraints均已从主包导出:

export type { MapConstraints } from './arbitrary/map.js'; export { map } from './arbitrary/map.js'; // ... export type { SetConstraints } from './arbitrary/set.js'; export { set } from './arbitrary/set.js';

新增fc.maparbitrary

基本用法与签名

fc.map的调用方式与fc.dictionary、fc.object类似,接收 key 和 value 两个 arbitrary:

// 生成字符串键、数字值的 Map fc.map(fc.string(), fc.nat()); // 带约束条件 fc.map(fc.string(), fc.nat(), { minKeys: 1, maxKeys: 10 });

完整签名为:

fc.map(keyArb, valueArb) fc.map(keyArb, valueArb, { minKeys?, maxKeys?, size?, depthIdentifier? })

各约束参数(定义于 MapConstraints):

参数默认值含义
keyArb必填生成 Map 键的 arbitrary
valueArb必填生成 Map 值的 arbitrary
minKeys?0生成实例中条目数的下限(含)
maxKeys?0x7fffffff生成实例中条目数的上限(含)
size?全局默认(small)控制生成值“能有多大”,详见下文 size 机制
depthIdentifier?undefined在多个实例间共享当前深度,避免递归结构过深

实现原理:uniqueArray + SameValueZero 去重

map.ts 的实现只有几行,但信息量很大:

export function map<K, V>( keyArb: Arbitrary<K>, valueArb: Arbitrary<V>, constraints: MapConstraints = {}, ): Arbitrary<Map<K, V>> { return uniqueArray(tuple(keyArb, valueArb), { minLength: constraints.minKeys, maxLength: constraints.maxKeys, size: constraints.size, selector: mapKeyExtractor, depthIdentifier: constraints.depthIdentifier, comparator: 'SameValueZero', }).map(arrayToMapMapper, arrayToMapUnmapper); }

从中可以读出三个设计要点:

  1. 以[key, value]元组数组为中间表示。fc.map并不是凭空生成 Map,而是先生成一个“键唯一”的元组数组(uniqueArray(tuple(keyArb, valueArb), ...)),再映射为原生Map。selector: mapKeyExtractor告诉uniqueArray按元组的第一个元素(即 key)做去重判定,这正好与Map本身“键唯一”的语义对齐——两个SameValueZero相等的键在 Map 中本就会被合并。
  2. 键的比较采用comparator: 'SameValueZero'。这是Map内部使用的比较算法,因此生成器层面的“键唯一”与new Map(entries)构建时的行为完全一致,不会出现“数组里键看似重复、但 Map 里却保留了多个条目”的不一致。
  3. 注册了双向映射器。arrayToMapMapper / arrayToMapUnmapper 负责数组与Map之间的转换:
export function arrayToMapMapper<TKey, TValue>(data: [TKey, TValue][]): Map<TKey, TValue> { return new Map(data); } export function arrayToMapUnmapper<TKey, TValue>(value: unknown): [TKey, TValue][] { if (typeof value !== 'object' || value === null) { throw new Error('Incompatible instance received: should be a non-null object'); } if (!('constructor' in value) || value.constructor !== Map) { throw new Error('Incompatible instance received: should be of exact type Map'); } return Array.from(value as Map<TKey, TValue>); }

map/unmap成对注册是 fast-check 的关键机制:有了 unmap(逆变换),生成过的 Map 才能被 shrink 器重新展开为元组数组,进而收缩到更小的 Map。unmap 端严格校验constructor === Map,确保只有“确实由该 arbitrary 生产出来的值”才参与后续收缩,而不是深入检查值的内部。

测试如何验证 fc.map 的正确性

map.spec.ts 使用了一套“约束参数自己也是随机的”集成测试范式:

const extraParameters: fc.Arbitrary<Extra> = fc.record( { keys: fc.uniqueArray(fc.anything(), { minLength: 35, // Keys equal in terms of SameValueZero will be merged together in a Map. comparator: 'SameValueZero', }), values: fc.uniqueArray(fc.anything(), { minLength: 1 }), constraints: fc .tuple(fc.nat({ max: 5 }), fc.nat({ max: 30 }), fc.boolean(), fc.boolean()) .map(([min, gap, withMin, withMax]) => ({ minKeys: withMin ? min : undefined, maxKeys: withMax ? min + gap : undefined, })), }, { requiredKeys: ['keys', 'values'] }, );

它覆盖三类断言:

  • assertProduceSameValueGivenSameSeed—— 同种子必得同值(可复现性);
  • assertProduceCorrectValues—— 生成的Map中每个 key、value 都必须来自指定的候选集合(精确引用相等,NaN 单独判断),且minKeys/maxKeys约束被遵守;
  • assertProduceValuesShrinkableWithoutContext—— 在底层 arbitrary 可收缩的前提下,生成的 Map 本身也被视为“可收缩”。

新增fc.setarbitrary

基本用法与签名

与fc.map同期引入的fc.set用于生成Set实例:

// 生成字符串集合 fc.set(fc.string()); // 带大小约束 fc.set(fc.nat(), { minLength: 2, maxLength: 20 });

完整签名为:

fc.set(arb) fc.set(arb, { minLength?, maxLength?, size?, depthIdentifier? })

各约束参数(定义于 SetConstraints):

参数默认值含义
arb必填生成集合元素的 arbitrary
minLength?0集合元素数量下限(含)
maxLength?0x7fffffff集合元素数量上限(含)
size?全局默认控制生成值规模
depthIdentifier?undefined共享深度标识,约束递归深度

SameValueZero:与原生 Set 完全一致的相等语义

公告强调:“fc.set保证生成的集合中所有元素按Set的相等语义唯一”。这一点在 set.ts 中落实为对uniqueArray的调用:

export function set<T>(arb: Arbitrary<T>, constraints: SetConstraints = {}): Arbitrary<Set<T>> { return uniqueArray(arb, { minLength: constraints.minLength, maxLength: constraints.maxLength, size: constraints.size, depthIdentifier: constraints.depthIdentifier, comparator: 'SameValueZero', }).map(arrayToSetMapper, arrayToSetUnmapper); }

SameValueZero与Object.is几乎相同,唯一区别是将-0与+0视为相等——这正是Set和Map使用的算法。其实际影响可以用一组边界值直观理解:

  • NaN与NaN相等(而===下不等),所以集合中至多一个NaN;
  • -0与+0相等,所以集合中不会同时出现两者。

set.spec.ts 中专门有一条针对这些特殊数值的测试:

it('should handle special numeric values correctly (NaN, -0, +0)', async () => { await assertProduceCorrectValues( () => set(constantFrom(-0, 0, Number.NaN, 1, 2)), (s) => { // In SameValueZero, -0 and +0 are considered equal, so we shouldn't have both const zeroCount = Array.from(s).filter((v) => v === 0).length; return zeroCount <= 1; }, ); });

转换映射器 ArrayToSet.ts 与 Map 版本同构:正向new Set(data),反向校验constructor === Set后Array.from展开,同样保证了 Set 的可收缩性。

另外需要留意源码注释中的一条边界说明:depthIdentifier目前只作用于“集合内部元素”的深度控制,不会反过来影响“本次要生成多少个元素”的决策(见 SetConstraints.depthIdentifier 的 JSDoc)。

fc.dictionary 支持完整 PropertyKey

变更前后对比

JavaScript 对象的属性键(PropertyKey)可以是字符串、数字或 Symbol,但 4.4.0 之前的fc.dictionary只接受Arbitrary<string>。4.4.0 起,它接受完整的PropertyKey范围。

这一变化在源码中体现为 dictionary.ts 的一组重载:

// 旧签名:字符串键,返回 Record<string, T>(自 1.0.0 起) export function dictionary<T>( keyArb: Arbitrary<string>, valueArb: Arbitrary<T>, constraints?: DictionaryConstraints, ): Arbitrary<Record<string, T>>; // 新签名:完整 PropertyKey 键,返回 Record<K, V>(自 4.4.0 起) export function dictionary<K extends PropertyKey, V>( keyArb: Arbitrary<K>, valueArb: Arbitrary<V>, constraints?: DictionaryConstraints, ): Arbitrary<Record<K, V>>;

由于第一个重载依然保留,既有字符串键的调用方式与返回类型完全向后兼容;数字与 Symbol 键则走新重载,返回Record<K, V>。

实现细节:数字键的字符串化与__proto__的安全处理

运行时实现部分(第 85–101 行)与fc.map结构一致——uniqueArray(tuple(keyArb, valueArb), { selector: dictionaryKeyExtractor, ... })生成键唯一的键值对数组,再经keyValuePairsToObjectMapper构建对象。这里有两个值得注意的实现细节,都在 KeyValuePairsToObject.ts 中:

const obj: Record<K, V> = definition[1] ? Object.create(null) : {}; for (let idx = 0; idx !== keyValues.length; ++idx) { const key = keyValues[idx][0]; if (key === '__proto__') { Object.defineProperty(obj, key, { enumerable: true, configurable: true, writable: true, value: keyValues[idx][1], }); } else { obj[key] = keyValues[idx][1]; } }
  • __proto__特殊分支:普通赋值obj['__proto__'] = v触发的是原型链设置而非定义自有属性;改用Object.defineProperty才能把__proto__作为真正的自有数据属性写入。对属性测试工具而言,这一处理确保了{'__proto__': value}这类“看起来危险”的输入被当作普通属性来生成,而不是悄悄改变对象的原型。
  • 数字键会被隐式转成字符串:这是 JS 语言语义(对象属性名始终是字符串或 Symbol)。dictionary.spec.ts 的测试中专门用注释和 selector 反映了这一点:
keys: fc.uniqueArray(fc.oneof(fc.string(), fc.nat(), fc.string().map(Symbol), { // Numbers will become strings when used in an object. selector: (key) => (typeof key === 'number' ? String(key) : key), }),

该测试同时覆盖了三种键类型:fc.string()、fc.nat()、fc.string().map(Symbol),并验证minKeys/maxKeys/noNullPrototype约束均正确生效。

fc.dictionary的约束参数(DictionaryConstraints)在此版本中保持不变:minKeys(默认 0)、maxKeys(默认0x7fffffff)、size、depthIdentifier(3.15.0 起)、noNullPrototype(3.13.0 起,默认false;实现中通过noNullPrototype ? constant(false) : boolean()决定是否随机生成 null-prototype 对象)。

size 机制:三个 arbitrary 共享的规模控制

fc.map、fc.set、fc.dictionary都支持size参数,其语义集中在 MaxLengthFromMinLength.ts:

  • Size 取值:'xsmall' | 'small' | 'medium' | 'large' | 'xlarge',全局默认是small(DefaultSize);也支持相对值('-4'到'+4')与'max';
  • 实际上限:maxLength的统一上界是MaxLengthUpperBound = 0x7fffffff,即各约束表中maxKeys/maxLength的默认值;
  • 由 size 推导实际长度上限的公式(maxLengthFromMinLength):
xsmall → floor(1.1 × min) + 1 small → 2 × min + 10 medium → 11 × min + 100 large → 101 × min + 1000 xlarge → 1001 × min + 10000

也就是说,不显式写maxLength时,生成规模由minLength(或minKeys)和size共同决定;显式写了maxKeys时结果还会与公式值取较小者。对fc.map/fc.set的常见实践是:键/值来自一个很小的有限集合(如fc.constantFrom('a','b','c'))时,把maxKeys设为可区分的键数上限,可以避免生成器反复尝试生成注定重复的条目。

depthIdentifier则是为递归结构准备的:相同标识的实例共享一份深度计数,集合内已生成的条目越多,后续条目越倾向“变浅”,从而避免结构在深度上失控膨胀。

4.4.0 的其余变更

官方 changelog(基于 4.3.0)除上述三项功能外还包含:

Features

  • fc.dictionary支持完整PropertyKey(PR#6232);
  • 新增fc.map(PR#6267);
  • fc.letrec的circular选项(PR#6040)——注意:同一版本内即被 PR#6334 以REVERT-6040撤销,即“letrec的自引用能力”在 4.4.0 中并未真正落地,阅读 changelog 时不要把这条当作可用特性;
  • 新增fc.set(PR#6270)。

Fixes 与其他(摘要)

修复项大部分是 CI 与发布流水线层面的调整,例如:发布时强制 OTP、npm 发布增加 provenance attestation(PR#6184)、pnpm lockfile 合并冲突的自动解决工作流(PR#6248)、测试矩阵调整到 Node 24 并分片产生覆盖率(PR#6282/PR#6283)等。值得留意的代码层变更有两条:

  • 性能(PR#6278):在性能关键路径上改用Math.imul与位移运算;
  • 重构(PR#6311/PR#6318/PR#6320/PR#6321):letrec实现抽取公共部分、惰性 arbitrary 构建逻辑抽出、LazyArbitrary显式空值检查等——这些是fc.map/fc.set落地前对集合类 arbitrary 基础设施的整理。

完整清单(含全部 PR 编号)见 4.4.0 公告 与 CHANGELOG。

小结与实践建议

  • 被测代码以Map为输入输出时,直接用fc.map(keyArb, valueArb, { minKeys, maxKeys })替代手搓fc.uniqueArray(...).map(new Map),键唯一性由SameValueZero保证且自带 shrink;
  • fc.set对NaN、±0的去重行为与原生Set一致,测试涉及浮点边界值时这一点尤其重要;
  • 需要 Symbol 键或数字键的对象生成(如序列化器、缓存键映射的测试)现在可以直接交给fc.dictionary,无需再通过fc.record之类的结构间接表达;
  • 键来自有限集合时,显式给出maxKeys/maxLength能显著提升生成效率;
  • 以上 API 自 fast-check 4.4.0 引入,当前仓库中的主包版本(见 package.json 的version字段)已经包含这些能力,官方文档中的签名与示例见 Object 组合类 arbitrary 文档。
  • 开发工具
  • 测试

【免费下载链接】fast-check

Property based testing framework for JavaScript (like QuickCheck) written in TypeScript

项目地址:https://gitcode.com/gh_mirrors/fa/fast-check
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/23 10:38:54

高维球体体积为何随维度崩塌?解读维数灾难与机器学习

“高维球体的体积是多少&#xff1f;”——第一次看到这个问题&#xff0c;很多人的第一反应是&#xff1a;球体体积不是早就学过了吗&#xff1f;但细想一下&#xff0c;你学过的公式是三维的。如果球体跑到四维、五维、十维、一百维&#xff0c;它的体积怎么算&#xff1f;这…

作者头像 李华
网站建设 2026/10/24 1:00:44

Matlab水文预报日模型与次洪模型代码实战:从解压到率定排错

简介&#xff1a;一个基于MATLAB的水文预报代码包&#xff0c;涵盖日模型与次洪模型两大模块&#xff0c;面向水文学研究者、水利工程师及高校相关专业学生&#xff0c;用于流域产流量预测与洪水过程线模拟。压缩包共21个文件&#xff0c;包含17个CSV数据文件&#xff08;1989—…

作者头像 李华
网站建设 2026/10/23 10:32:11

用PyTorch实现RNN手写数字识别:从图像序列化到工程避坑

简介&#xff1a;这是一份基于PyTorch实现RNN手写数字识别的入门实战代码包&#xff0c;面向正在学习深度学习与序列建模的开发者&#xff0c;演示如何用循环神经网络处理MNIST图像序列分类任务。压缩包仅1个文件、大小约2KB&#xff0c;主体为一份可运行的Python脚本&#xff…

作者头像 李华
网站建设 2026/10/21 20:40:33

SpringBoot+Vue构建二手手机交易管理系统

1. 项目背景与核心价值华强北作为全球知名的电子产品集散地&#xff0c;二手手机交易一直是其核心业务之一。传统线下交易模式存在信息不透明、价格混乱、质量难保障等痛点&#xff0c;而简单的电商平台又难以满足专业二手手机交易的特殊需求&#xff08;如验机报告、零件溯源、…

作者头像 李华
网站建设 2026/10/21 20:41:18

Win32 GDI捕鱼游戏开发:从窗口消息到贝塞尔鱼群动画

简介&#xff1a;本资源是一份面向计算机专业本科生的Windows平台C课程设计实践项目——“捕鱼达人”小游戏源码包&#xff0c;聚焦Windows API图形编程与面向对象游戏逻辑实现&#xff0c;适用于《Windows程序设计》《游戏开发基础》等课程实训及C进阶学习。压缩包共36个文件&…

作者头像 李华
网站建设 2026/10/22 20:19:02

电子劳动合同避坑指南:从平台选择到归档留存,照着做不出错

10 月 9 日&#xff0c;甘肃省人社厅发布了一份电子劳动合同的科普说明&#xff0c;澎湃等平台做了转载。内容不算新&#xff0c;但讲得很系统——把订立电子劳动合同要走的环节、要满足的条件&#xff0c;按顺序梳理了一遍。这类官方说明值得看的原因在于&#xff0c;它把平时…

作者头像 李华