← 中文资源

简体中文版本

用属性测试覆盖业务规则

用 fast-check 生成输入,断言必须恒成立的不变量,并通过收缩把失败化简为最小可复现用例。

阅读英文原文

概述

手写测试本质上是文档,记录的是作者想到的场景,无法覆盖“总金额 × 人数”这样的输入空间。属性测试反过来:先声明一条对所有合法输入都必须成立的规则,再由生成器搜索反例。第一个反例往往没人会手写,例如 100 分钱分给 3 个人。

步骤

  1. 安装 fast-check。
  2. 先写不变量并对着当前实现运行,确认它能失败:
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 });
  1. 修正实现:取整后用余数逐一分摊。
const base = Math.floor(totalCents / people);
const remainder = totalCents % people;
return Array.from({ length: people }, (_, i) => base + (i < remainder ? 1 : 0));
  1. 增加第二个属性:最大份额与最小份额之差不超过 1 分,且没有负数。
  2. 把收缩后的反例写成显式示例(examples: [[100, 3]]),确保随机种子变化时回归仍会执行。

验证

修复前属性应报 total=100, people=3 失败,三个份额为 [33,33,33],总和 99。修复后 500 次生成全部通过,100 分钱分给 3 人得到 [34,33,33]。日志中应打印种子,便于失败时精确复现。

注意事项

  • 属性只能证明你声明的内容,写错的不变量会永远通过。
  • 生成器要遵守业务约束(人数至少为 1),否则失败可能来自非法输入而不是产品缺陷。
  • 金额一律使用整数分,浮点数会让每个属性都变成舍入讨论。
  • 高 numRuns 占用 CPU,适合放到夜间任务,本地保持较小次数。
  • 属性测试擅长纯函数、解析、序列化与状态机,不能替代数据库约束与集成测试。