fast-check 4.8.0 发布解读:chainUntil 任意值与迭代式链式生成
开发工具测试【免费下载链接】fast-checkProperty based testing framework for JavaScript (like QuickCheck) written in TypeScript项目地址https://gitcode.com/gh_mirrors/fa/fast-check点击查看免费下载fast-check 4.8.0 是 4.x 系列中以「组合器」为主题的版本核心特性是新增fc.chainUntil任意值它把过去必须靠层层嵌套.chain才能表达的「不确定深度的依赖生成」变成一个循环并且对很长的链也支持完整的 shrink收缩。本篇基于仓库中的发布说明release blog、chainUntil 源码、单元测试 和 回归测试完整讲解这个 API 的用法、底层生成与收缩机制以及它在后续版本中被entityGraph内部复用的事实。为什么需要chainUntil.chain的嵌套困境先回顾 fast-check 中依赖生成的标准手段.chain。它是Arbitrary类上的实例方法签名与实现在 Arbitrary.ts// packages/fast-check/src/check/arbitrary/definition/Arbitrary.ts chainU(chainer: (t: T) ArbitraryU): ArbitraryU;典型用法如fc.nat().chain((n) fc.integer({ max: n }))先生成一个数再基于它生成下一个数。问题在于如果你想在生成结果上再依赖一次生成结果就必须再嵌套一层.chainfc.nat().chain((n) fc.nat(n).chain((m) fc.nat(m) // 想生成几层就要预先写几层 ) );链的深度在编写代码时就必须确定。而属性测试中大量真实场景是「长度由前面的随机值决定」的生成一条直到某个随机条件不满足才停止的序列、生成深度不定的链表节点、生成按关系逐级展开的实体图。这类变长、数据驱动的依赖生成用静态嵌套的.chain无法优雅表达——要么硬编码一个上限深度要么放弃。4.8.0 的chainUntil就是为此设计的迭代式组合器。chainUntil的签名与用法从 源码入口 看API 只有两个参数并且通过 fast-check.ts 主入口 导出为fc.chainUntilfc.chainUntilT( startArb: ArbitraryT, // 起始任意值产出链条的第一个值 chainer: (prev: T) ArbitraryT | undefined // 用当前值决定下一个任意值返回 undefined 即停止 ): ArbitraryT工作流是先用startArb生成第一个值循环调用chainer(当前值)它返回「下一步该用哪个任意值」一旦chainer返回undefined循环结束整个chainUntil输出的值是链条最后一步产生的那个值不是整条链的快照除非你像官方示例那样让值本身携带历史。发布说明中的官方示例同样收录于 文档站 arbitrary 页面fc.chainUntil( // Start from a tuple containing one value in 0..20, fc.nat(20).map((n) [n]), // Then, if value is greater than 10, append another value in 0..20 (tuple) (tuple.at(-1) 10 ? fc.nat(20).map((n) [...tuple, n]) : undefined), ); // Examples of generated values: [14,6], [2], [1], [20,2], [18,17,13,3]…这段代码的语义是从一个只含 1 个0..20自然数的数组出发只要数组最后一个元素大于 10就再往尾部追加一个0..20的数。生成的数组长度完全由随机数据本身决定——可能是 1 个元素起点就 ≤10也可能是任意多个。注意chainer里判断的是tuple.at(-1) 10这利用了「只有 10 的数还能继续生 10 的数」这一结构使链条既能变长又保证终止。这个模式可以推广到很多变长结构变长序列startArb产出一个头节点chainer根据当前节点决定是否追加下一个依赖型累积值每一步的生成范围由上一步结果约束如文档中fc.nat(n).chain(...)的循环版图/树的逐实体构建维护一个「已生成状态」对象每步在其中追加一个实体直到达到目标规模——这正是后文entityGraph内部的做法。源码解读完全迭代且收缩「跟着链条走」ChainUntilArbitrary的完整实现只有约 150 行位于 chainUntil.ts。它的两个设计重点是生成阶段记录链条、收缩阶段按层重放。生成阶段循环 每步快照随机源generate 方法 的核心逻辑// 简化自 packages/fast-check/src/arbitrary/chainUntil.ts generate(mrng: Random, biasFactor: number | undefined): ValueT { const entries: ChainUntilEntryT[] []; let current this.startArb.generate(mrng, biasFactor); entries.push({ arbitrary: this.startArb, value: current.value_, context: current.context, clonedMrng: mrng.clone() }); while (true) { const nextArb this.chainer(current.value_); if (nextArb undefined) break; const nextClonedMrng mrng.clone(); current nextArb.generate(mrng, biasFactor); entries.push({ arbitrary: nextArb, value: current.value_, context: current.context, clonedMrng: nextClonedMrng }); } return new Value(current.value_, { biasFactor, entries, currentShrinkLevel: 0 }); }两个关键细节非递归的while循环链条多长都只是一次函数调用内的循环不会因链长增长调用栈。测试 chainUntil.spec.ts 中的专门用例 先测量了「朴素递归会在多深时溢出」的阈值computeMaximalStackSize()然后构造两倍于该阈值的链长断言同样深度的朴素递归会抛RangeError而chainUntil的 generate 与 shrink 都正常完成。每一步都把当时的随机源状态克隆下来clonedMrng: mrng.clone()。这些快照是收缩阶段「确定性重放后半段链条」的燃料见下文。生成上下文还包含currentShrinkLevel用于记忆「收缩进度推进到了第几层」使得 runner 多次调用shrink时可以从上次位置继续而不是从头重算。收缩阶段逐层收缩 克隆随机源重放shrinkIterator 的算法可以概括为「先收缩早层、再收缩晚层收缩某一层后用该层保存的随机源快照把后续链条原样重生成一遍」// 简化自 packages/fast-check/src/arbitrary/chainUntil.ts for (let level currentShrinkLevel; level entries.length; level) { const entry entries[level]; for (const shrunkValue of entry.arbitrary.shrink(entry.value, entry.context)) { const newEntries entries.slice(0, level); // 保留前 level 层不变 newEntries.push({ ...entry, value: shrunkValue.value_, context: shrunkValue.context }); const mrng entry.clonedMrng.clone(); // 用该层生成前克隆的随机源 let current shrunkValue; while (true) { // 重放后续链条 const nextArb this.chainer(current.value_); if (nextArb undefined) break; const next nextArb.generate(mrng, biasFactor); newEntries.push({ arbitrary: nextArb, value: next.value_, context: next.context, ... }); current next; } yield new Value(newEntries.at(-1).value, { biasFactor, entries: newEntries, currentShrinkLevel: level }); } }这套机制带来三个可验证的行为收缩顺序从早到晚单元测试 「should shrink earlier levels before later levels」 构造startArb(10) - arb(20)的两层链断言第一个收缩候选来自收缩起点10→5链条随后重放第二个才是收缩第二层20→15。收缩可能改变链长「should handle chains where shrinking changes the chain length」 中链条长度由数值本身决定≥3 继续、3 停止。起点从 5 收缩到 4 后重新执行chainer得到的链条整体变短最终值也随之变化——由于重放是「重新调用chainer」而不是机械搬移旧条目这种数据驱动的链长变化能被正确处理。收缩树结构完整「should produce the right shrinking tree for a simple chain」 用大写/小写编码各层是否被收缩快照断言了完整的收缩树L0-L1-L2 ├ l0-L1-L2 | ├ l0-l1-L2 | | └ l0-l1-l2 | └ l0-L1-l2 ├ L0-l1-L2 | └ L0-l1-l2 └ L0-L1-l2每一层都是独立的可收缩节点且任意一层收缩后更深层会基于新的前缀继续展开。另外注意canShrinkWithoutContext返回falsechainUntil的收缩强依赖生成时保存的entries上下文见 shrink 入口上下文不合法时直接返回空迭代器。这意味着它不能像fc.nat()那样对「无上下文的值」做兜底收缩但换来的是对任意嵌套链条的精确控制。回归测试证据失败案例能被持续收缩到最小仓库的 NoRegression.spec.ts 对每个主要任意值都有一个「故意失败 快照断言」的回归用例chainUntil的用例是it(chainUntil, async () { await expectPropertyToThrowErrorMatchingSnapshot( fc.property( fc.chainUntil( fc.nat().map((n): number[] [n]), (tuple) (tuple[tuple.length - 1] 10 ? fc.nat().map((n) [...tuple, n]) : undefined), ), (v) testFunc(v), ), ); });对应的失败快照保存在 NoRegression.spec.ts.snap[Error: Property failed after 1 tests { seed: 42, path: 0:1:0:..., endOnFailure: true } Counterexample: [[11,0]] Shrunk 30 time(s) Execution summary: × [[2147483626,2014944498,4]] . √ [[0]] . × [[1073741813,2147483626,2014944498,4]] ...逐步减半收缩过程这个快照很有信息量首次命中失败的样本是一个 3 元素大数组经过30 次收缩后得到最小的反例[[11,0]]——11 是恰好 10、还能触发链条延续的最小自然数0 是追加值的最小收缩结果说明收缩确实沿着链条结构收敛到了语义上最小的失败输入快照中还出现了√ [[0]]收缩过程中链长从 3 变到 1说明「收缩改变链长」这一路径在端到端 runner 里同样成立该用例以快照形式被 CI 锁定任何让chainUntil收缩行为发生回归的改动都会直接暴露。框架内部的第一位「正式用户」entityGraphchainUntil不只是给用户用的组合器fast-check 团队自己也用它重构了fc.entityGraph的链接生成逻辑。从 OnTheFlyLinksForEntityGraphArbitrary.ts 可以看到export function onTheFlyLinksForEntityGraph(...): ArbitraryProducedLinks... { assertAcceptableRelations(relations); const inversedRelations buildInversedRelationsMapping(relations); const initialStateArb constant(buildInitialProductionState(relations, defaultEntities)); return chainUntil(initialStateArb, (state) { if (state.nextIndex state.toBeProducedEntities.length) { return undefined; // 所有实体都处理完链条结束 } // 找到下一个「有相关链接可建」的实体构造对应的 step arbitrary ... return next; }).map((state) state.producedLinks); }结构上是教科书式的chainUntil用法constant产出初始的「生产状态」对象chainer每步在状态中推进一个实体把它的关系链接构建出来直到nextIndex越过待生成实体总数时返回undefined。CHANGELOG 中 4.9.0 的条目「Towards shrinkableentityGraphthanks tochainUntil」PR#7008正是基于这一步重构说明 4.8.0 引入的迭代链条为entityGraph的收缩能力打下了基础。entityGraph本身的用法可参考 文档站说明。4.8.0 的其余变更除chainUntil外CHANGELOG 中 4.8.0 段落 记录的变更主要是修复与工程化按主题归类Bug 修复PR#6965 恢复了「不使用skipLibCheck」的构建选项能力即类型声明文件不再强依赖用户打开skipLibCheck测试修复PR#6934 让文档测试容忍\p{...}Unicode 属性值的演进漂移PR#6951 修复了 poisoning 包在最新 Node 上的测试文档修复修复 API 参考页失效链接PR#6900、把手动配置指南抽成独立页面PR#6844、为文档各分区补充索引页PR#6845、修正 Documentation 入口链接PR#6918CI 调整一批流水线调整包括 pnpm 全局虚拟存储、npm install中固定 pnpm 版本、安全依赖覆盖改用 tilde 区间、仓库镜像到 tangled 平台等。对用户而言与升级行为直接相关的主要是 Bug 与测试两项CI 与文档条目不影响运行时的库行为。升级与适用前提最低版本chainUntil自 4.8.0 起可用在 文档站 中标注为 Available since 4.8.0源码 JSDoc 同样声明remarks Since 4.8.0。当前仓库packages/fast-check的版本号为 4.10.2见 package.json即 4.8.0 之后还经历了 4.9.0 对entityGraph的增强与 4.10.x 的 plugin 化改造升级时以实际 changelog 为准使用限制chainer必须在有限步内返回undefined否则生成循环不会终止这与它「无限递归链会栈溢出」的旧问题不同——现在会表现为永不返回收缩依赖上下文如前所述canShrinkWithoutContext为false收缩只在完整走 generate → shrink 的 runner 流程中生效手动对无上下文的值调用shrink会得到空迭代器chainer会被多次调用不仅 generate 时调用shrink 重放时也会反复调用因此chainer内部应保持轻量、无副作用避免在其中读取外部可变状态。小结fast-check 4.8.0 用约 150 行非递归实现解决了「变长依赖生成」这一长期痛点fc.chainUntil(startArb, chainer)把.chain的固定深度嵌套换成显式循环通过每步克隆随机源 逐层收缩 确定性重放让链条任意长度下都具备完整的 shrink 能力单元测试 覆盖了长链不溢出、收缩树结构、链长随收缩变化等关键路径NoRegression 快照 锁定了端到端收缩行为而entityGraph的内部重构则证明了它在框架自身复杂场景中的可用性。如果你的属性测试中出现「生成几层取决于前面生成什么」的模式值得升级到 4.8.0 并改用chainUntil。赞分享开发工具测试【免费下载链接】fast-checkProperty based testing framework for JavaScript (like QuickCheck) written in TypeScript项目地址https://gitcode.com/gh_mirrors/fa/fast-check点击查看免费下载相关推荐fast-check 3.11.0 发布详解内置 ulid 任意值生成器与安全质量改进fast check 3.11.0 发布详解内置 ulid 任意值生成器与安全质量改进 本篇文章基于 fast check 仓库中 3.11.0 版本发布说明开发工具测试fast-check 3.17.0 发布解读把随机值生成下沉到 Worker 线程fast check 3.17.0 发布解读把随机值生成下沉到 Worker 线程 导读 fast check 3.17.0 的核心变化只有一个却意义深远开发工具测试fast-check 3.21.0 新特性uuid 任意值生成器支持自定义版本v1–v15fast check 3.21.0 新特性uuid 任意值生成器支持自定义版本v1–v15 本篇技术指南聚焦 fast check 3.21.0 版本对开发工具测试创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考