资讯动态

fast-check 实战:用「Advent of PBT Day 1」破解黑盒排序算法 Bug

发布时间:2026/10/14 9:07:09 来源:尧图企业网站定制
开发工具测试【免费下载链接】fast-checkProperty based testing framework for JavaScript (like QuickCheck) written in TypeScript项目地址https://gitcode.com/gh_mirrors/fa/fast-check点击查看免费下载本文以 fast-check 官方博客系列Advent of PBT 2024的第一天挑战为实战案例讲解如何针对黑盒排序算法编写属性测试Property-Based Testing通过随机化输入与收缩shrinking机制定位并复现潜在缺陷。读完本文你将掌握fc.property、fc.array、fc.nat、fc.stringMatching等核心构件的组合用法并能独立复现 Day 1 的完整解题过程与实现源码细节。挑战背景Santa 的信件排序系统圣诞老人每年都会收到来自全球孩子和大人的海量信件每封信都带有独一无二的愿望清单与留言。为了在大日子前及时回复圣诞老人今年决定按以下规则给回信排序发件人越年轻回复越快按年龄升序排列年龄相同时按名字使用操作符做字典序排序。输入被定义为一个信件数组每封信是一个形如nameage的字符串name由小写字母a-z构成、至少一个字符age为7到77之间的整数。精灵们被要求返回一个按上述规则排序后的新数组。小精灵们按期交付了实现但圣诞老人担心其中暗藏缺陷——他无法直接看到实现源码黑盒于是寄希望于 fast-check 的属性测试能力找出能让精灵实现崩溃或产生错误输出的一组输入。这正是 Day 1 挑战页 的核心任务。先看懂「黑盒」从源码反推缺陷机理虽然挑战设计为黑盒不直接展示实现但仓库中其实保留了精灵们交付的源码文件 buggy.mjs便于我们事后验证推理过程// ts-check /** typedef {{name: string; age: number;}} Letter */ /** * param {Letter[]} letters * returns {Letter[]} */ export default function sortLetters(letters) { const clonedLetters [...letters]; return clonedLetters.sort( (la, lb) la.age - lb.age || (la.name.codePointAt(0) ?? 0) - (lb.name.codePointAt(0) ?? 0), ); }而挑战页通过 AdventOfTheDay.tsx 注入了参考实现reference implementation用于比对行为差异type Letter { name: string; age: number }; function sortLetters(letters: Letter[]): Letter[] { const clonedLetters [...letters]; return clonedLetters.sort((la, lb) la.age ! lb.age ? la.age - lb.age : la.name lb.name ? 1 : la.name lb.name ? -1 : 0, ); }两相对照缺陷立即显形实现年龄比较同名比较参考实现la.age ! lb.age ? la.age - lb.age : …完整三态比较// 相等返回 0精灵实现buggyla.age - lb.age \|\| …仅用codePointAt(0)比较首字符两个关键缺陷同名字母序不完整精灵实现只比较名字的第一个字符la.name.codePointAt(0) - lb.name.codePointAt(0)而非像运算符那样做完整字典序比较。例如alice与adam首字符相同但后续字符不同该实现会把它们误判为“相等”。codePointAt(0)返回undefined时的兜底值代码里?? 0意味着当名字为空字符串时会按0参与比较。虽然题目约束“名字至少一个字符”规避了空名但这也暴露出实现者对该 API 边界情况的处理方式。用 fast-check 构造反例核心属性测试代码挑战页的交互式答题面板要求参与者手工构造反例输入并提交验证其输入格式由 AdventOfTheDay.tsx 中的解析器定义const lineRegex /^([a-z])(\d)$/; function parser(answer: string): unknown[] | undefined { const parsedAnswer: Letter[] []; for (const line of answer.split(\n)) { const m lineRegex.exec(line); if (m null) { throw new Error( Each line of the answer should follow the pattern: nameage, …, ); } parsedAnswer.push({ name: m[1], age: Number(m[2]) }); } return [parsedAnswer]; }也就是说答案形如anne15每行一封信且要求age在7到77之间。这正是 fast-check 展示其价值的典型场景与其靠直觉手写一组输入不如用属性测试自动搜索反例。下面是用 fast-check 编写的解题属性import * as fc from fast-check; import { sortLetters } from ./advent.js; // 被测实现 // 定义“合法信件”生成器名字 小写字母串年龄 ∈ [7, 77] const letterArbitrary fc.record({ name: fc.stringMatching(/^[a-z]$/), age: fc.integer({ min: 7, max: 77 }), }); // 属性 1排序结果必须保持“年龄优先、同年龄按完整字典序” // 通过对照参考实现差分测试验证黑盒行为 fc.assert( fc.property(fc.array(letterArbitrary), (letters) { const actual sortLetters(letters); const expected referenceSort(letters); // 参考实现 // 逐项断言结果一致可用 lodash 的 isEqual // 若两实现输出不同则说明发现反例 }), );关键构件说明均来自仓库官方文档fc.property声明“要验证什么谓词 如何生成谓词输入arbitraries”的最小单元支持同步与异步谓词fc.array生成数组可传{ minLength?, maxLength?, size? }控制长度分布默认minLength: 0、maxLength: 0x7ffffffffc.stringMatching生成匹配给定正则的字符串这里是约束name为纯小写字母串的精确手段fc.integer在闭区间[min, max]内生成有符号整数天然满足age ∈ [7, 77]的取值约束fc.record组合出{ name, age }结构的信件对象。当属性断言失败时fc.assertrunner会自动尝试收缩shrinking——把导致失败的最小输入逐步缩减为人类可读的最简单反例这是 fast-check 相较纯随机测试的核心优势见 getting-started 文档 对 runner 的说明。反例剖析一个反例如何击穿精灵实现结合上文源码分析攻击点显而易见只需构造两个年龄相同、名字首字符相同但完整字典序不同的信件例如alice15 adam15按题目规则运算符adam alice正确顺序应为adam在前但精灵实现用codePointAt(0)比较a - a 0两封信被判为“相等”排序结果取决于Array.prototype.sort的稳定性alice可能仍然排在adam前面从而输出错误顺序。同理bob30与ben30、carol40与carl40都能稳定击穿该实现。在 Day 1 挑战的答题表单中提交形如alice15\nadam15的输入即可验证参考实现与精灵实现的输出不一致代表解题成功。这正是属性测试的威力差分测试differential testing——拿一个可信参考实现与黑盒实现做输入输出比对——能在不知道实现细节的情况下自动发现语义偏差。从挑战到工程实践排序类算法可复用的属性集Day 1 的题目本质上是对一个排序函数的黑盒验证。仓库中的 examples/001-simple/sort 提供了一个不依赖参考实现、直接面向排序语义的完整属性集范本可作为同类场景的通用模板import { describe, it, expect } from vitest; import * as fc from fast-check; import _ from lodash; import { sort } from ./src/sort.js; describe(sort, () { it(should have the same length as source, async () { await fc.assert( fc.property(fc.array(fc.integer()), (data) { expect(sort(data)).toHaveLength(data.length); }), ); }); it(should have exactly the same number of occurences as source for each item, async () { await fc.assert( fc.property(fc.array(fc.integer()), (data) { const sorted sort(data); expect(_.groupBy(sorted)).toEqual(_.groupBy(data)); }), ); }); it(should produce an ordered array, async () { await fc.assert( fc.property(fc.array(fc.integer()), (data) { const sorted sort(data); for (let idx 1; idx sorted.length; idx) { expect(sorted[idx - 1]).toBeLessThanOrEqual(sorted[idx]); } }), ); }); });这套属性集覆盖了排序算法的三条不变量完全适用于 Day 1 场景只需把比较器换成“年龄优先、同名完整字典序”长度不变排序不增不减元素多重集合不变每个元素出现的次数与输入一致用groupBy验证防止元素被替换或丢失有序性按指定比较器相邻元素满足顺序关系。其中第 2、3 条对 Day 1 尤其致命精灵实现会“吞掉”同首字符名字间的次序差异一旦属性 3 使用题目定义的完整字典序比较器反例就会被自动搜出并收缩到最小规模。挑战页工程解析黑盒答题如何运作如果你好奇挑战页“黑盒 表单提交”的机制仓库源码给出了完整答案。整个 Day 1 页面由三个组件协作AdventOfTheDay.tsx把精灵实现buggyAdvent、参考实现referenceAdvent与输入解析器组装成可交互的 Playground 与答题表单AdventOfTheDayBuilder.tsx核心比对逻辑所在。提交答案后onSubmit会调用isEqual(postAdvent(buggyFn(...inputs)), postAdvent(referenceAdvent(...inputs)))——若两者输出相等则提示“你提供的输入看起来工作正常圣诞老人在找 bug”若不等则判定反例有效并存入localStorage键为aopbt24-day1下次访问自动回填AdventPlayground.tsx通过 StackBlitz SDK 动态嵌入一个包含advent.js、advent.d.ts、advent.test.ts与package.json依赖fast-check、vitest的 Node 模板项目让读者在浏览器里直接跑npx vitest --watch进行属性测试实验同时提供本地下载入口便于离线复现。这套机制本身就是对属性测试工作流的一个绝佳示例被测对象隔离黑盒 参考实现对照 可复现的最小输入与我们在“反例剖析”一节做的事完全一致。小结从一道圣诞谜题到可复用的测试方法论Advent of PBT Day 1 表面上是一道节日谜题内核却浓缩了属性测试的三个关键实践用差分测试攻破黑盒拿参考实现做输入输出对照无需阅读源码即可定位语义偏差让生成器忠实于规格用fc.stringMatching(/^[a-z]$/)与fc.integer({ min: 7, max: 77 })把输入空间精确约束在题目定义域内避免生成无效数据依赖收缩定位最小反例fc.assert在断言失败时自动把反例缩减到最易读的形式如[{name:alice,age:15},{name:adam,age:15}]让修复者一眼看清根因。如果你想继续深入可以顺着仓库线索依次阅读Day 1 挑战页、精灵缺陷实现、参考实现与输入解析以及官方文档中的 属性与 runner 入门 和 arbitraries 总览。祝你圣诞快乐也祝你早日用属性测试驯服所有黑盒算法。赞分享开发工具测试【免费下载链接】fast-checkProperty based testing framework for JavaScript (like QuickCheck) written in TypeScript项目地址https://gitcode.com/gh_mirrors/fa/fast-check点击查看免费下载相关推荐TypeScript 联合类型实战TypeHero Advent of TypeScript 2024 Day 1Christmas Cookies题解与题目机制剖析TypeScript 联合类型实战TypeHero Advent of TypeScript 2024 Day 1Christmas Cookies题解与教育前端后端用 TypeScript 类型系统实现倒计时TypeHero「Advent of TypeScript 2023」Day 13 的 DayCounter 实战解析用 TypeScript 类型系统实现倒计时TypeHero「Advent of TypeScript 2023」Day 13 的 DayCounter 实战教育前端后端TypeHero 题解用 TypeScript 变长元组类型实现 SantasListAdvent of TypeScript 2024 Day 5TypeHero 题解用 TypeScript 变长元组类型实现 SantasList Advent of TypeScript 2024 Day 5 本教育前端后端上一篇GraalVM Truffle 静态对象模型Static Object Model完全指南从 API 入门到 Shape 层次与安全优化下一篇魔兽争霸3在新系统上闪退、UI模糊、帧率锁30WarcraftHelper 五分钟全部搞定创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑