等价类和边界值处理的是“一个输入能取哪些值”,把无穷取值收敛成几条[article:04629bda]。但结算、审批、计费这类需求,麻烦不在单个输入上:用券还是打折、满不满 99、会员是哪一档,三件事叠起来才决定最后收多少钱。判定表就是干这件事的工具,顺带还带来一个副作用要处理:条件一多,列数会失控。

先把规则给全

案例是编的,规则原文只有四条。请先自己试着列出要验哪几种情况,再看下面的解法。

  1. 会员折扣:银卡按商品原价 95 折,金卡 9 折。
  2. 平台券“满 99 减 15”,一单限用一张,原价满 99 才能用。
  3. 会员折扣与平台券不可同享:用了券就不再打折。
  4. 商品原价满 99 元包邮,否则收 8 元运费。运费按原价判断,与优惠无关。

最后半句是故意加的,它决定后面能砍掉多少列,也是真实需求里最容易被省略的那句话。

第一步:分清条件和动作,别把“哪一档”写成两个条件

表的左边是 条件桩,也就是所有会左右结果的因素;左下是 动作桩,也就是系统可能做出的所有响应。表格主体一列一条 规则:一组条件取值,对应一组动作。

有个很常见的错:为了整齐,写成“是金卡”“是银卡”“是否用券”三个布尔条件。这样会排出“既是金卡又是银卡”的列,现实中不存在,还得多花力气逐列标注哪个不可能。

正确的写法是:会员等级是一个条件,取值是“银卡/金卡”。所以列数不是固定 2n2^n,而是各条件取值个数相乘:

规则数=∏i(第 i 个条件的取值个数)\text{规则数}=\prod_i (\text{第 } i \text{ 个条件的取值个数})

本例是 2×2×2=82\times2\times2=8 列。这条要记住,后面算规模全靠它,不是靠数条件个数。

第二步:先铺满,不要一边排一边合并

R1R2R3R4R5R6R7R8
条件:会员等级银卡银卡银卡银卡金卡金卡金卡金卡
条件:是否用券不用不用用用不用不用用用
条件:原价满 99是否是否是否是否
动作:打会员折扣✓✓✓✓
动作:减 15✓✓
动作:收运费 8✓✓

先铺满再精简,是为了让“漏了哪一列”看得见。边排边凑的表,漏掉的列永远不会出现在纸面上。

第三步:删掉造不出来的列,再合并同一动作的列

不可行组合 要整列删掉。R4 和 R8 都是“用券 + 原价不满 99”,而规则 2 写明原价不满 99 用不了券:这两列造不出真实数据,留着要么被误当用例去跑,要么被随手标成“不测”。

删完剩六列。接着看能否合并:规则 3 说用了券就不再打折,所以 R3 和 R7 里会员等级取什么都不改变动作。这时把该格写成 “–”,即无关项,两列并成一列,意思是“这一条件在这里不影响结果”。

T1T2T3T4T5
会员等级银卡银卡金卡金卡–
是否用券不用不用不用不用用
原价满 99是否是否是
打会员折扣✓✓✓✓
减 15✓
收运费 8✓✓
覆盖的原始组合数11112

核对办法(checksum):把“覆盖的原始组合数”加起来,应等于删掉不可行列之后的组合总数。这里 1+1+1+1+2=61+1+1+1+2=6,对得上。合并最容易犯的错,是把动作其实不同的两列并成一列;一旦并错,合计立刻对不上。

顺带说清口径:这类表的 100% 覆盖按 可行列 算,不是按 2n2^n 算 5。

第四步:把每列变成能跑的用例

五列是五组逻辑,每组至少一条用例。具体填什么数,交给等价类和边界值:门槛 99 那列要取 98.99、99.00、99.01。

对着规则算四条,用来验证表本身对不对:

  • T3(金卡、不用券、原价 120):120×0.9=108120\times0.9=108,原价满 99 包邮,应付 108。
  • T4(金卡、不用券、原价 98):98×0.9=88.2098\times0.9=88.20,不满 99 收 8 元运费,应付 96.20。
  • T2(银卡、不用券、原价 99):99×0.95=94.0599\times0.95=94.05,包邮,应付 94.05。
  • T5(任意等级、用券、原价 99):99−15=8499-15=84,包邮,应付 84。

第四条最容易实现错:优惠后只有 84 元却包邮,因为包邮看的是原价。开发若顺手用优惠后金额去比 99,就会多收 8 元,而前三条都发现不了。这正是条件叠在一起才生出的缺陷,也是判定表值得写的原因。

(顺带一句:99.01×0.9=89.10999.01\times0.9=89.109 该舍到分还是到角,需求里没写。判定表不解决它,但会把这个问题逼到纸面上——写表本身就是一次需求检查。)

怎么判断这张表没写漏

没有别人帮你判卷,用这四条自查:

  1. 合计对得上:各列覆盖的原始组合数之和,等于可行组合总数。
  2. 每列都能造出数据:造不出来的,要么是漏删的不可行组合,要么是条件写错了。
  3. 每个动作至少出现一次:某列什么动作都没有,通常说明需求在这一格没写清。
  4. 每个条件都至少改变过一次结果:某个条件在任何列里都不影响动作,只有两种解释——它在需求里其实没用,或者你漏了一条规则。这是判定表最好用的地方:它不只排用例,还能把需求的空洞顶出来。

条件一多,列数会失控

真实的大促结算远不止三个条件。假设还有:会员三档、是否新客首单、是否大促时段、支付方式三种、是否偏远地区,加上是否用券,六个条件的取值个数分别是 3、2、2、2、3、2:

3×2×2×2×3×2=1443\times2\times2\times2\times3\times2=144

条件再多一个就是几百列。全测不现实,随手挑几列又不叫测过。

通用的做法是 成对覆盖(pairwise):不要求覆盖全部组合,只要求任意两个条件的取值组合都至少在一列里出现过。144 列能被压到十几条,而覆盖面并不虚——NIST 统计过 NASA 的一个应用,67% 的失效只由一个参数取值触发,93% 由两个参数的组合触发,98% 由三个参数的组合触发;在他们收集的数千份失效报告里,没有一例涉及超过六个变量的交互 34。这类只有特定组合才发作的问题叫 交互失效,成对覆盖正是冲着它去的。

用之前要知道三条边界:

  • 它是概率上的够用,不是证明。 需要三路、四路才发作的缺陷,两路覆盖很可能漏掉。涉及资金计算、权限判断、历史事故复发的组合,即使条件多也要显式保留,或者把强度提到三路。
  • 前提是取值离散。 连续取值要先按等价类收敛成代表值,否则“每两个取值的组合”无从谈起。
  • 不可行组合先剔掉,否则生成的组合里会有一批根本跑不起来。

收束成三步

  1. 列条件,按取值个数而不是条件个数算规模,互斥的取值合成一个多值条件。
  2. 铺满、删不可行、合并无关项,再用合计核对。
  3. 条件少就直接全列,多了改成对覆盖,把关键组合手工补回表里。

最后交代一处不适用。判定表假设结果只取决于“此刻哪些条件同时成立”,与判断的先后顺序无关。如果结果依赖顺序——订单从待支付到已支付、超时取消、退款重入——同一组条件换个顺序就是另一种结果,那时该用状态迁移图 5。