☰
fast-check 4.7.0 深度解析:stringMatching 支持 Unicode 属性转义与可逆的 json 生成器
2026/10/14 15:16:01 网站建设 项目流程
  • 开发工具
  • 测试

【免费下载链接】fast-check

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

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

fast-check 4.7.0 是一个围绕"让生成器更贴近真实输入"展开的发布:stringMatching从此支持正则中的 Unicode 属性转义\p{...}/\P{...},fc.json()则通过引入基于JSON.parse的 unmapper 变为可逆(reversible)arbitrary。本文基于 4.7.0 官方发布说明(website/blog/2026-04-18-whats-new-in-fast-check-4-7-0/index.md),结合仓库源码,逐项讲解这两项特性的用法、实现原理与测试验证方式,帮助你为国际化输入、Emoji 处理以及生产环境 JSON 回放等场景构建更贴近真实数据的属性测试。

版本概览:4.7.0 基于 4.6.0

4.7.0 直接基于 4.6.0 构建(4.6.0 的卖点是stringMatching的maxLength支持,见 CHANGELOG)。该版本的功能(Features)变更如下:

  • Reversiblejsonarbitrary(PR#6866)
  • Parse\p{}and\P{}instringMatching(PR#6868)
  • Support for\p{UnicodeProperty}instringMatching(PR#6870)
  • Support negated unicode properties instringMatching(PR#6871)

其余为 CI、文档与测试相关的 Fixes 类提交(如将 race condition 测试迁移到 Vitest、稳定 Windows 上的 flaky timeout 测试等),与核心 API 无关。

stringMatching 支持 Unicode 属性转义

解决的问题

正则表达式可以用 Unicode 属性转义来限定字符集,例如\p{Emoji}匹配 Emoji、\P{ASCII}匹配所有非 ASCII 字符。在 4.7.0 之前,把这样的正则交给fc.stringMatching会抛出 "not implemented yet" 错误;从 4.7.0 起,stringMatching同时识别\p{...}(肯定形式)与\P{...}(否定形式),可以直接生成匹配的值:

// Strings made of emoji characters // e.g. "🦎👭👇🚀🕍", "🍻😁🧀🪠🕹" fc.stringMatching(/^\p{Emoji}+$/u); // One uppercase letter followed by one or more lowercase letters // e.g. "Gụ", "F𞤻applyg" fc.stringMatching(/^\p{Uppercase_Letter}\p{Lowercase_Letter}+$/u); // Strings made only of non-ASCII characters // e.g. "𸉂􏿼", "𿝘𖈝" fc.stringMatching(/^\P{ASCII}+$/u);

注意:\p{...}/\P{...}转义需要正则带u(unicode)标志才能正确按代码点语义工作,官方文档示例(website/docs/core-blocks/arbitraries/combiners/string.md)中也是以fc.stringMatching(/^\p{Emoji}$/u)的形式给出的。

支持的属性形式

从源码中的属性解析表 UnicodePropertyData.ts 可以确认支持范围与官方说明一致:

  • 二进制属性:如Emoji、Alphabetic(别名Alpha)、Math、ASCII、Cased等,共 50 余个(见 UnicodePropertyData.ts 的BINARY_PROP_NAMES_TO_ALIASES表),支持规范名与短名(shorthand)互换;
  • 通用类别(General_Category):如Letter(L)、Uppercase_Letter(Lu)、Lowercase_Letter(Ll)、Decimal_Number(Nd)、Punctuation(P)等(见 UnicodePropertyData.ts 的类别表);
  • Script 属性:通过Script=Greek、sc=Han、Script_Extensions等形式使用,脚本值与四字母缩写互相可解析(如Greek↔Grek、Han↔Hani,见 UnicodePropertyData.ts)。

resolveUnicodeProperty会把\p{...}大括号内的内容解析为结构化 token:先按=拆分显式形式(\p{Name=Value}),再依次尝试 General Category 值、二进制属性名,并把短名归一化为规范名;无法识别的属性会抛出Unknown Unicode property name/value或Invalid Unicode property错误(UnicodePropertyData.ts)。

底层实现:属性 → 码点区间 → 字符类机制

发布说明提到"每个属性会被展开为其覆盖的码点集合,并接入既有的字符类机制,且结果只计算一次并缓存"。这与源码完全对应,调用链是:

  1. 分词阶段:stringMatching内部先用tokenizeRegex把正则 source 分词。遇到\p/\P开头的块时,调用resolveUnicodeProperty(propertySpec, negative)产出UnicodeProperty类型的 token(见 TokenizeRegex.ts);该 token 既可出现在正则主体中,也可出现在字符类[...]内。
  2. AST 转换阶段:toMatchingArbitrary遇到UnicodeProperty节点时直接委托给unicodePropertyArbitrary(见 stringMatching.ts)。
  3. 区间计算阶段:UnicodePropertyArbitraryHelper.ts 是核心。它构造一个"探测正则"^\p{Spec}$(否定形式则用^\P{Spec}$),然后从 0x0 到 0xD7FF 与 0xE000 到 0x10FFFF 逐码点测试,把连续的命中码点压缩成区间列表(appendRangesForRegex,注意跳过了 0xD800–0xDFFF 代理区,见 UnicodePropertyArbitraryHelper.ts);
  4. 缓存:区间结果以p:/P:Spec为 key 缓存在模块级Map中(UnicodePropertyArbitraryHelper.ts)。首次使用某个属性需要一次全码点遍历(约 110 万次regex.test),但同一进程内后续使用直接命中缓存,这正是"computed once and cached"的出处。
  5. 接入字符类机制:每个区间被convertGraphemeRangeToMapToConstantEntry转换为mapToConstant的条目,最终以mapToConstant(...rangeEntries)形式产出一个在属性覆盖范围内均匀采样的字符 arbitrary,从而复用了 fast-check 既有的区间→constant 映射与收缩机制。

从源码结构看,这种"探测式展开"意味着生成的值与引擎的 Unicode 属性数据保持一致(因为直接用RegExp测试每个码点),同时也意味着属性名拼写错误会在分词阶段尽早报错,而不是等到生成时才失败。

测试验证

集成测试 stringMatching.spec.ts 将/^\p{Emoji}+$/u与/^\P{Emoji}+$/u加入了硬编码正则池,并通过assertProduceSameValueGivenSameSeed(同 seed 产生同值)和assertProduceCorrectValues(用"新建同 source/flags 的正则"对每个生成值做test校验,避免依赖 RegExp 实例内部状态,见 stringMatching.spec.ts)双重验证。你可以用同样的思路在本地快速验证:

import * as fc from 'fast-check'; fc.assert( fc.property(fc.stringMatching(/^\p{Emoji}+$/u), (s) => { return /^\p{Emoji}+$/u.test(s); }), );

可逆的 json arbitrary

背景:单向映射的局限

fc.json()的实现是"用JSON.stringify序列化jsonValue的输出"(发布说明原话)。在 4.7.0 之前这个映射是单向的:fast-check 能产出 JSON 字符串,但无法识别外部提供的 JSON 字符串。后果是,凡是需要回放(replay)或重组(recompose)值的场景——比如收缩一段从生产日志里捕获的 payload——都无法借力fc.json():当框架拿到一个没有生成上下文(context)的值时,不知道如何把它映射回jsonValue的值空间,也就无从收缩。

实现:给 map 补上 unmapper

4.7.0 的实现非常简洁——给map补上第二个参数 unmapper(基于JSON.parse):

// packages/fast-check/src/arbitrary/json.ts function jsonStringUnmapper(value: unknown): JsonValue { if (typeof value !== 'string') { throw new Error('Cannot unmap the passed value'); } return JSON.parse(value) as JsonValue; } export function json(constraints: JsonSharedConstraints = {}): Arbitrary<string> { const arb = jsonValue(constraints); return arb.map(JSON.stringify, jsonStringUnmapper); }

(见 json.ts)

Arbitrary.map(mapper, unmapper?)的 unmapper 正是"仅在没有上下文、需要收缩用户定义值时才使用,且遇到不兼容的值必须抛错"的钩子(Arbitrary.ts)。MapArbitrary收到无上下文的 shrink 请求时,会先用 unmapper 把值还原到底层 arbitrary 的值空间,再走正常收缩(Arbitrary.ts)。

使用效果:canShrinkWithoutContext 变为 true

发布说明给出的最小验证示例:

const arb = fc.json(); arb.canShrinkWithoutContext('{"a":{"b":[1,2,3]}}'); // now true

在此之前,对外部传入的合法 JSON 字符串,canShrinkWithoutContext返回false;现在任意合法 JSON 字符串都会被jsonStringUnmapper接受并可收缩。这意味着fc.json()可以作为一个"可逆积木"参与组合,也能在需要回放 JSON 输入而无需从头重新生成时作为 drop-in 方案。

集成测试 json.spec.ts 中新增了针对可逆性的断言:

it('should produce values seen as shrinkable without any context', async () => { await assertProduceValuesShrinkableWithoutContext(jsonBuilder, { extraParameters }); });

该测试同时保留了同 seed 一致性校验(assertProduceSameValueGivenSameSeed)与正确性校验(用JSON.parse不抛错 +maxDepth约束检查来验证输出)。测试文件中还有一处值得注意的注释:assertShrinkProducesSameValueWithoutInitialContext并不适用于json——因为JSON.stringify(-0)产出"0"但JSON.parse("0")得到0(而非-0),且数字键的顺序会因Object.keys变化,两条收缩路径可能分叉(json.spec.ts)。这说明可逆性是指"值空间可往返",而非"逐比特等价"。

fc.json()仍支持原有的约束参数JsonSharedConstraints(如maxDepth、depthSize、stringUnit、noUnicodeString),可逆化并未改变其生成侧 API。

实践建议与边界

结合本次发布与源码,给出几条可操作的结论:

  1. 正则要带u标志:\p{...}/\P{...}的语义依赖 unicode 模式;stringMatching本身支持d、g、m、s、u五个标志,不支持i(大小写不敏感)与y(sticky),传入不支持的标志会直接抛错(stringMatching.ts)。
  2. 首次使用某属性有一次性成本:属性到区间的展开是全码点探测(0x0–0xD7FF、0xE000–0x10FFFF 两段),但结果按属性缓存于模块级Map,同一测试进程内重复使用同一属性无额外开销;不同属性名/别名(如Letter与L)会各自归一化后计算,缓存 key 基于规范化后的Spec。
  3. 属性支持范围以仓库内映射表为准:UnicodePropertyData.ts中的三张表(二进制属性、通用类别、Script)定义了当前支持的全部属性与别名;表外名称会在解析阶段抛出明确的错误信息,便于快速定位拼写问题。
  4. fc.json()的回放语义:任何能被JSON.parse接受的字符串都会被收缩,但-0、键顺序等细节在 stringify/parse 往返中不保证保留(见上文测试注释),对这类边缘语义敏感的场景需自行注意。
  5. 升级路径:若你从 4.6.0 升级到 4.7.0,API 均为增量能力(stringMatching新增可解析的正则形式、fc.json()行为放宽),不存在破坏性变更;stringMatching的maxLength/size约束在 4.7.0 中保持不变(stringMatching.ts)。

小结

fast-check 4.7.0 的两个特性都指向同一个方向——让 arbitrary 能处理"不是 fast-check 自己生成的输入":stringMatching通过把 Unicode 属性转义展开为码点区间并接入既有的字符类机制,使 Emoji、大小写字母、非 ASCII 等国际化正则可直接用于生成;fc.json()通过JSON.stringify/JSON.parse成对的 mapper/unmapper 变得可逆,使外部 JSON 字符串可以被识别和收缩。两者分别由 stringMatching.ts、UnicodePropertyArbitraryHelper.ts、json.ts 及对应 spec 文件中的测试用例共同佐证,可以直接在当前仓库中复核。

  • 开发工具
  • 测试

【免费下载链接】fast-check

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

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

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询