- 开发工具
- 测试
【免费下载链接】fast-check
Property based testing framework for JavaScript (like QuickCheck) written in TypeScript
导读
本文围绕 fast-check 仓库 .changeset/petite-eyes-relax.md 中记录的变更「💥 Drop global capture of methods [Batch 3]」展开,梳理 fast-check 4.x 对“捕获全局方法”这一历史机制的完整清理。在 4.x 中,fast-check 不再依赖、也不再建议用户通过临时改写Date.now()、Math.random()等全局 API 来获取可控随机性,而是将所有确定性随机性收敛到自身的 PRNG(伪随机数生成器)与 Arbitrary 组合器中。读完本文,你将理解该变更的技术背景、它在仓库中对应的三批 changeset 与相关源码证据,以及如何在测试代码中完成对应的迁移。
一、变更内容:fast-check标记为 major 的 breaking change
.changeset/petite-eyes-relax.md 的全文如下:
--- "fast-check": major --- 💥 Drop global capture of methods [Batch 3]这是一条典型的 Changesets 变更记录:"fast-check": major表示该变更会被归类为 fast-check 包的下一个 major 版本(即 4.x)中的破坏性变更;正文使用💥前缀标识 breaking change,主题是Drop global capture of methods(移除对全局方法的捕获),并标注为「Batch 3」。
二、这不是孤立的一条:三批 methods + 一批 functions 的完整清理
「Batch 3」意味着在此之前还有同主题的批次。仓库 .changeset 目录中与“global capture”相关的 changeset 共有四条:
| 文件 | 变更级别 | 内容 |
|---|---|---|
| .changeset/rare-lemons-prove.md | "fast-check": major | 💥 Drop global capture of methods [Batch 1] |
| .changeset/late-guests-brush.md | "fast-check": major | 💥 Drop global capture of methods [Batch 2] |
| .changeset/petite-eyes-relax.md | "fast-check": major | 💥 Drop global capture of methods [Batch 3] |
| .changeset/ten-clubs-smile.md | "fast-check": major | 💥 Drop global capture of functions |
也就是说,fast-check 4.x 分四个批次彻底移除了两类“全局捕获”能力:
- methods(方法):对
Date、Math等内置对象上方法(如Date.now()、Math.random())的临时捕获; - functions(函数):对全局函数的捕获。
从 changeset 编号看,methods 的移除分为三个批次推进,而 functions 的移除独立成一条,二者共同构成 4.x 中“全局捕获机制”的整体下线。
为什么分批次?
从仓库结构看,fast-check 核心包 packages/fast-check 是单一发布单元,此类涉及面广的破坏性变更会被拆成多个 PR、多个批次逐步合并,便于审查、回归与文档迁移。分批次发布在 changeset 层面表现为多条相同主题的记录,最终在 changelog 中合并呈现。
三、技术背景:fast-check 为什么曾经需要“捕获全局”
要理解该变更的破坏性,需要先理解 fast-check 生成随机值的机制。
3.1 确定性随机来自 Arbitrary + PRNG,而非全局 API
fast-check 的随机性全部由内部 PRNG(种子驱动)产生,并通过 Arbitrary 组合器分发。以 日期生成器 为例,它并不调用new Date()或Date.now()去“碰运气”,而是基于整数 Arbitrary 映射得到时间戳:
// packages/fast-check/src/arbitrary/date.ts(节选) import { integer } from './integer.js'; import { timeToDateMapper, ... } from './_internals/mappers/TimeToDate.js'; export interface DateConstraints { min?: Date; // 默认 new Date(-8640000000000000) // ... }浮点数生成同样如此,float.ts 通过integer({ min, max })索引映射到 IEEE-754 位模式,NaN 也由索引范围显式控制:
// packages/fast-check/src/arbitrary/float.ts(节选) return integer({ min: minIndexWithNaN, max: maxIndexWithNaN }).map( (index) => { /* index → float 位模式,超出范围即 NaN */ } );而Math.random()、Date.now()这类全局 API 在 fast-check 核心源码中并不作为随机源。唯一出现Date.now的位置是 runner 配置中默认 seed 的兜底取值,见 QualifiedParameters.ts:
const safeDateNow = Date.now; // 先取引用,避免后续被污染 // ... if (p.seed === undefined) return safeDateNow() ^ (Math.random() * 0x100000000);注意这里有两个关键点:
- 只有当用户没有显式传入 seed时,才会用
Date.now()异或Math.random()生成一个“随机初始种子”;一旦指定 seed,运行完全确定; - 源码用
const safeDateNow = Date.now;在模块加载时先保存引用,这正是“防污染(poisoning-free)”的写法——即使后续有人改写了全局Date.now,fast-check 仍使用最初捕获的版本。
3.2 旧机制:为测试可控性临时改写全局 API
在早期的属性测试实践中,一种常见的做法是:为了让被测代码中的Math.random()、Date.now()变得可控,测试框架会在运行期间**临时替换(capture/global patch)**这些全局方法,让它们从 fast-check 的 PRNG 取值,跑完再恢复。这就是 changeset 中所说的 “global capture of methods”。
fast-check 4.x 之所以要将其彻底移除,从仓库证据可以推断:
- 核心包本身已不依赖它:如上文所示,fast-check 的确定性全部由 PRNG + Arbitrary 保证,捕获全局方法并非核心路径;
- 它制造了隐式、不可组合的副作用:改写全局对象会影响同进程中的其他测试、库和被测代码,破坏隔离性;在 worker、并行执行、异步调度场景下尤其危险;
- 仓库对“全局污染”本身持零容忍态度:fast-check 甚至单独维护了一个 @fast-check/poisoning 附加包,用于检测原型污染与全局污染(详见 main.ts 与其 测试)。一个“检测污染”的库,自然不愿意在自己的核心运行时里继续做“改写全局”这种危险动作。
四、仓库中的“防污染”哲学:Safe-* 与 Poisoning-Free
虽然本次 changeset 讨论的是移除“捕获全局”,但 fast-check 源码中处处体现着“既不捕获、也不被污染”的设计,可作为理解该变更的旁证:
- PoisoningFreeArray.ts、PoisoningFreeMap.ts、PoisoningFreeSet.ts 在模块加载时即保存
Array.prototype.map、Map.prototype.get等方法的引用,保证即使原型被篡改,内部逻辑仍可用原始实现; - CaptureAllGlobals.ts 是检测工具:它遍历
globalThis的可达对象,记录快照,用于发现“谁偷偷改了全局”。注意,这里的 “capture” 是捕获快照用于审计,与旧版 fast-check 的“改写全局用于生成随机值”是两种相反的机制; - runner 配置层的
safeDateNow同样是“先保存、后使用”的防污染写法。
因此,本次 changeset 的意图可以概括为:fast-check 4.x 把自己的行为边界收回到“只读全局、绝不改写全局”。
五、对使用者的影响与迁移建议
5.1 直接影响
- 如果你的测试代码曾依赖 fast-check 旧版本“自动改写
Date.now()/Math.random()等全局方法”这一隐含行为,在 4.x 中该行为已不复存在; - 需要随机性的场景,请显式使用 fast-check 的 Arbitrary(如 date、float、double、uuid 等),它们都由 seed 驱动、完全可复现,并通过 shrinking 在失败时给出最小反例;
- 若你仍需要让被测代码中的“真随机”可复现,推荐做法是注入:将被测模块依赖的随机源(函数、时钟)作为参数注入,再由 fast-check 的 Arbitrary 生成对应的种子值。
5.2 迁移示例(注入式改造)
把「依赖全局Math.random()」的被测代码:
function pickRandom(items) { return items[Math.floor(Math.random() * items.length)]; }改为「随机源可注入」:
function pickRandom(items, rng = Math.random) { return items[Math.floor(rng() * items.length)]; }然后在 fast-check 属性中,用fc.integer({ min: 0, max: 0xffffffff })之类的 Arbitrary 生成种子,并构造确定性的rng(例如 fast-check 自带的 PRNG)注入被测函数。这样既保留了 fast-check 的确定性、可复现性与 shrinking,又不再触碰任何全局状态。
5.3 从源码确认:生成器本身已“全局无关”
可以从仓库源码逐层验证“核心生成器不读全局”:
- func.ts 生成纯函数:用
tuple(array(arb, { minLength: 1 }), noShrink(integer()))构造输出表与种子,函数体内以hash(\${seed}${stringify(args)}`)` 决定返回值; - compareFunc.ts 与 compareBooleanFunc.ts 委托 CompareFunctionArbitraryBuilder.ts,同样以
hash(${seed}${reprA}) % hashEnvSize计算比较结果; - float32Array.ts 等 TypedArray 生成器通过
Float32Array.from(data)由内部数组映射而来,见 float64Array.ts 的同类实现; - uuid.ts 基于
integer与 PaddedNumberArbitraryBuilder.ts 的位拼接,而非crypto.randomUUID()。
以上全部依赖 seed + PRNG,无任何对全局 API 的运行时捕获,这从实现层面印证了 changeset 声明的可行性。
六、相关官方文档与迁移资源
如果你正在从 fast-check 3.x 升级到 4.x,可以结合以下仓库文档进一步确认影响面:
- 迁移指南 website/docs/migration/from-3.x-to-4.x.md:涵盖
date(默认生成 Invalid Date 的问题)、.noBias/.noShrink迁移、Random实例方法调整、runner hooks 显式调用等 4.x 破坏性变更; - 函数类 Arbitrary 官方说明 website/docs/core-blocks/arbitraries/composites/function.md:展示了
fc.func(arb)、fc.compareFunc()、fc.compareBooleanFunc()的签名与生成示例; - 假数据与随机性替代方案 website/docs/advanced/fake-data.md:讲解如何把外部假数据库(如 Faker)包装为 Arbitrary,以及如何用 fast-check 控制随机源,是“不再依赖全局捕获”场景下的直接实践参考;
- 原型污染检测 website/blog/2023-09-21-detect-prototype-pollution-automatically/index.md:介绍 @fast-check/poisoning 包的背景,与本次“移除全局捕获”同属 fast-check 对全局环境安全性的整体策略。
七、小结
.changeset/petite-eyes-relax.md 记录的「Drop global capture of methods [Batch 3]」不是一条孤立的破坏性变更,而是 fast-check 4.x 中“彻底下线全局捕获机制”的收尾批次之一:它与 Batch 1、Batch 2 以及独立的 functions 批次一起,将 fast-check 的随机性彻底收敛到 seed 驱动的 PRNG 与 Arbitrary 组合器内部。对使用者而言,升级到 4.x 后应放弃任何依赖“框架自动改写全局方法”的写法,改为显式使用 Arbitrary 或注入式随机源设计,从而获得更干净、更可复现、与全局环境完全解耦的属性测试体验。
- 开发工具
- 测试
【免费下载链接】fast-check
Property based testing framework for JavaScript (like QuickCheck) written in TypeScript
相关推荐
fast-check 变更解读:从 changeset 看 FrequencyArbitrary 防御性检查的移除
fast check 变更解读:从 changeset 看 FrequencyArbitrary 防御性检查的移除 本篇围绕 fast check 仓库中的一条
开发工具测试pixi global remove 命令完全指南:从全局环境精准移除依赖
pixi global remove 命令完全指南:从全局环境精准移除依赖 pixi global remove 是 pixi 全局工具链中用于 从指定全局环境
开发工具CLI包管理器任务调度career-ops:AI驱动的求职系统终极指南——14种技能模式与自动化工作流全解析
career ops:AI驱动的求职系统终极指南——14种技能模式与自动化工作流全解析 career ops是一款基于Claude Code构建的AI驱动求职系
人工智能AI 应用AI 技能
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考