概述
手写测试本质上是文档,记录的是作者想到的场景,无法覆盖“总金额 × 人数”这样的输入空间。属性测试反过来:先声明一条对所有合法输入都必须成立的规则,再由生成器搜索反例。第一个反例往往没人会手写,例如 100 分钱分给 3 个人。
步骤
- 安装
fast-check。 - 先写不变量并对着当前实现运行,确认它能失败:
function splitMoney(totalCents, people) {
const share = Math.round(totalCents / people);
return Array(people).fill(share);
}
fc.assert(fc.property(
fc.integer({ min: 0, max: 1_000_000 }),
fc.integer({ min: 1, max: 20 }),
(total, people) => splitMoney(total, people).reduce((a, b) => a + b, 0) === total,
), { numRuns: 500 });
- 修正实现:取整后用余数逐一分摊。
const base = Math.floor(totalCents / people);
const remainder = totalCents % people;
return Array.from({ length: people }, (_, i) => base + (i < remainder ? 1 : 0));
- 增加第二个属性:最大份额与最小份额之差不超过 1 分,且没有负数。
- 把收缩后的反例写成显式示例(
examples: [[100, 3]]),确保随机种子变化时回归仍会执行。
验证
修复前属性应报 total=100, people=3 失败,三个份额为 [33,33,33],总和 99。修复后 500 次生成全部通过,100 分钱分给 3 人得到 [34,33,33]。日志中应打印种子,便于失败时精确复现。
注意事项
- 属性只能证明你声明的内容,写错的不变量会永远通过。
- 生成器要遵守业务约束(人数至少为 1),否则失败可能来自非法输入而不是产品缺陷。
- 金额一律使用整数分,浮点数会让每个属性都变成舍入讨论。
- 高
numRuns占用 CPU,适合放到夜间任务,本地保持较小次数。 - 属性测试擅长纯函数、解析、序列化与状态机,不能替代数据库约束与集成测试。