属性测试
声明函数必须对所有输入遵守的定律,让框架生成数百随机输入试图证伪它。
为什么
基于示例的测试查你想到的用例;bug 藏在你没想到的用例里。属性测试反转负担:你声明必须始终成立的属性(排序返回相同元素不同顺序;解析再序列化返回原值;折扣永不为负),框架生成数百随机输入试图找反例。找到时它把反例缩小到最小失败用例,常是你从未考虑的边界(空字符串、负零、25 小时时区偏移)。它找到再多示 例也到不了的 bug。
何时使用
用于有清晰定律的纯函数:解析器、序列化器、数学、排序、状态 reducer、数据变换。非副作用代码或 UI。函数有往返、逆或不变属性时加属性测试。
如何做
1. 选属性测试库
- JS/TS: fast-check
- Python: Hypothesis
- Rust: proptest 或 quickcheck
- Go: gopter
2. 确定一个属性(定律,非示例)
- 往返: parse(serialize(x)) === x
- 不变: sort(arr).length === arr.length
- 幂等: normalize(normalize(x)) === normalize(x)
- 单调: a > b → price(a) >= price(b)
3. 把属性写成对所有有效输入返回 true 的函数
- fc.assert(fc.property(fc.array(fc.integer()), (arr) => {
const sorted = sort(arr);
return sorted.length === arr.length; // 不变量
}))
4. 运行——框架默认生成 100+ 随机输入
- 全过 → 属性成立(高概率)
- 一个挂 → 框架缩小到最小反例
5. 读缩小反例修 bug 或修属性
- 缩小常启发人:"对 [] 挂"或"对 [0, 0] 挂"
- 若属性错了(太严)放宽它;若代码错了修它示例
import { describe, it, expect } from "vitest";
import fc from "fast-check";
import { parseDuration, formatDuration } from "./duration";
describe("时长往返", () => {
// 属性: 对任何有效时长,parse(format(d)) === d
it("parse ∘ format = 恒等", () => {
fc.assert(
fc.property(fc.integer({ min: 0, max: 86400 }), (seconds) => {
const formatted = formatDuration(seconds);
const reparsed = parseDuration(formatted);
expect(reparsed).toBe(seconds);
}),
);
});
// 属性: 格式化串永不含负数
it("format 永不产生负分量", () => {
fc.assert(
fc.property(fc.integer({ min: 0, max: 86400 }), (seconds) => {
const formatted = formatDuration(seconds);
expect(formatted).not.toMatch(/-/);
}),
);
});
});
// fast-check 发现: "对 3600 挂 → '1h0m0s' → parse 给 3600 ✓"
// 但若 formatDuration 对 0 有 bug: "对 0 挂 → '0h0m-0s'"
// 缩小反例精确定位边界用例。常见问题
- ⚠声明平凡为真的属性("返回字符串")给虚假信心——属性必须是若函数错则挂的有意义定律,非同义反复。
- ⚠无约束生成输入(fc.integer() 无界)产生生产中函数永不见的值(百亿元素数组),测试因与真实使用无关的原因挂。
- ⚠太严的属性("输出始终升序"对不排序的函数)老挂,开发者禁用它们而非修属性声明。
- ⚠忽略缩小反例只重跑到随机通过浪费属性测试最有价值的输出——缩小就是 bug,最小陈述。
最佳实践
- ✓从往返属性开始(parse ∘ serialize = 恒等)——最易陈述且抓最多序列化 bug,且无需正确输出预言。
- ✓把输入生成器约束到函数真实定义域(秒级时长用 fc.integer({ min: 0, max: 1000000 }))使失败反映真实输入非不可能的。
- ✓属性挂时仔细读缩小反例——它是最小失败用例,常揭示你未考虑的边界(空、零、负、最大边界)。
- ✓属性测试配几条示例测试:示例为读者记录具体期望行为,属性验证定律对所有输入成立。