ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

fast-check 4.x 变更解读:彻底移除对全局方法(global capture)的依赖

fast-check 4.x 变更解读:彻底移除对全局方法(global capture)的依赖
  • 开发工具
  • 测试

【免费下载链接】fast-check

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

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

导读

本文围绕 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 分四个批次彻底移除了两类“全局捕获”能力:

  1. methods(方法):对Date、Math等内置对象上方法(如Date.now()、Math.random())的临时捕获;
  2. 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 之所以要将其彻底移除,从仓库证据可以推断:

  1. 核心包本身已不依赖它:如上文所示,fast-check 的确定性全部由 PRNG + Arbitrary 保证,捕获全局方法并非核心路径;
  2. 它制造了隐式、不可组合的副作用:改写全局对象会影响同进程中的其他测试、库和被测代码,破坏隔离性;在 worker、并行执行、异步调度场景下尤其危险;
  3. 仓库对“全局污染”本身持零容忍态度: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

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

相关推荐

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进