等价类和边界值处理的是“一个输入能取哪些值”,把无穷取值收敛成几条[article:04629bda]。但结算、审批、计费这类需求,麻烦不在单个输入上:用券还是打折、满不满 99、会员是哪一档,三件事叠起来才决定最后收多少钱。判定表就是干这件事的工具,顺带还带来一个副作用要处理:条件一多,列数会失控。
先把规则给全
案例是编的,规则原文只有四条。请先自己试着列出要验哪几种情况,再看下面的解法。
- 会员折扣:银卡按商品原价 95 折,金卡 9 折。
- 平台券“满 99 减 15”,一单限用一张,原价满 99 才能用。
- 会员折扣与平台券不可同享:用了券就不再打折。
- 商品原价满 99 元包邮,否则收 8 元运费。运费按原价判断,与优惠无关。
最后半句是故意加的,它决定后面能砍掉多少列,也是真实需求里最容易被省略的那句话。
第一步:分清条件和动作,别把“哪一档”写成两个条件
表的左边是 条件桩,也就是所有会左右结果的因素;左下是 动作桩,也就是系统可能做出的所有响应。表格主体一列一条 规则:一组条件取值,对应一组动作。
有个很常见的错:为了整齐,写成“是金卡”“是银卡”“是否用券”三个布尔条件。这样会排出“既是金卡又是银卡”的列,现实中不存在,还得多花力气逐列标注哪个不可能。
正确的写法是:会员等级是一个条件,取值是“银卡/金卡”。所以列数不是固定 ,而是各条件取值个数相乘:
本例是 列。这条要记住,后面算规模全靠它,不是靠数条件个数。
第二步:先铺满,不要一边排一边合并
| R1 | R2 | R3 | R4 | R5 | R6 | R7 | R8 | |
|---|---|---|---|---|---|---|---|---|
| 条件:会员等级 | 银卡 | 银卡 | 银卡 | 银卡 | 金卡 | 金卡 | 金卡 | 金卡 |
| 条件:是否用券 | 不用 | 不用 | 用 | 用 | 不用 | 不用 | 用 | 用 |
| 条件:原价满 99 | 是 | 否 | 是 | 否 | 是 | 否 | 是 | 否 |
| 动作:打会员折扣 | ✓ | ✓ | ✓ | ✓ | ||||
| 动作:减 15 | ✓ | ✓ | ||||||
| 动作:收运费 8 | ✓ | ✓ |
先铺满再精简,是为了让“漏了哪一列”看得见。边排边凑的表,漏掉的列永远不会出现在纸面上。
第三步:删掉造不出来的列,再合并同一动作的列
不可行组合 要整列删掉。R4 和 R8 都是“用券 + 原价不满 99”,而规则 2 写明原价不满 99 用不了券:这两列造不出真实数据,留着要么被误当用例去跑,要么被随手标成“不测”。
删完剩六列。接着看能否合并:规则 3 说用了券就不再打折,所以 R3 和 R7 里会员等级取什么都不改变动作。这时把该格写成 “–”,即无关项,两列并成一列,意思是“这一条件在这里不影响结果”。
| T1 | T2 | T3 | T4 | T5 | |
|---|---|---|---|---|---|
| 会员等级 | 银卡 | 银卡 | 金卡 | 金卡 | – |
| 是否用券 | 不用 | 不用 | 不用 | 不用 | 用 |
| 原价满 99 | 是 | 否 | 是 | 否 | 是 |
| 打会员折扣 | ✓ | ✓ | ✓ | ✓ | |
| 减 15 | ✓ | ||||
| 收运费 8 | ✓ | ✓ | |||
| 覆盖的原始组合数 | 1 | 1 | 1 | 1 | 2 |
核对办法(checksum):把“覆盖的原始组合数”加起来,应等于删掉不可行列之后的组合总数。这里 ,对得上。合并最容易犯的错,是把动作其实不同的两列并成一列;一旦并错,合计立刻对不上。
顺带说清口径:这类表的 100% 覆盖按 可行列 算,不是按 算 5。
第四步:把每列变成能跑的用例
五列是五组逻辑,每组至少一条用例。具体填什么数,交给等价类和边界值:门槛 99 那列要取 98.99、99.00、99.01。
对着规则算四条,用来验证表本身对不对:
- T3(金卡、不用券、原价 120):,原价满 99 包邮,应付 108。
- T4(金卡、不用券、原价 98):,不满 99 收 8 元运费,应付 96.20。
- T2(银卡、不用券、原价 99):,包邮,应付 94.05。
- T5(任意等级、用券、原价 99):,包邮,应付 84。
第四条最容易实现错:优惠后只有 84 元却包邮,因为包邮看的是原价。开发若顺手用优惠后金额去比 99,就会多收 8 元,而前三条都发现不了。这正是条件叠在一起才生出的缺陷,也是判定表值得写的原因。
(顺带一句: 该舍到分还是到角,需求里没写。判定表不解决它,但会把这个问题逼到纸面上——写表本身就是一次需求检查。)
怎么判断这张表没写漏
没有别人帮你判卷,用这四条自查:
- 合计对得上:各列覆盖的原始组合数之和,等于可行组合总数。
- 每列都能造出数据:造不出来的,要么是漏删的不可行组合,要么是条件写错了。
- 每个动作至少出现一次:某列什么动作都没有,通常说明需求在这一格没写清。
- 每个条件都至少改变过一次结果:某个条件在任何列里都不影响动作,只有两种解释——它在需求里其实没用,或者你漏了一条规则。这是判定表最好用的地方:它不只排用例,还能把需求的空洞顶出来。
条件一多,列数会失控
真实的大促结算远不止三个条件。假设还有:会员三档、是否新客首单、是否大促时段、支付方式三种、是否偏远地区,加上是否用券,六个条件的取值个数分别是 3、2、2、2、3、2:
条件再多一个就是几百列。全测不现实,随手挑几列又不叫测过。
通用的做法是 成对覆盖(pairwise):不要求覆盖全部组合,只要求任意两个条件的取值组合都至少在一列里出现过。144 列能被压到十几条,而覆盖面并不虚——NIST 统计过 NASA 的一个应用,67% 的失效只由一个参数取值触发,93% 由两个参数的组合触发,98% 由三个参数的组合触发;在他们收集的数千份失效报告里,没有一例涉及超过六个变量的交互 34。这类只有特定组合才发作的问题叫 交互失效,成对覆盖正是冲着它去的。
用之前要知道三条边界:
- 它是概率上的够用,不是证明。 需要三路、四路才发作的缺陷,两路覆盖很可能漏掉。涉及资金计算、权限判断、历史事故复发的组合,即使条件多也要显式保留,或者把强度提到三路。
- 前提是取值离散。 连续取值要先按等价类收敛成代表值,否则“每两个取值的组合”无从谈起。
- 不可行组合先剔掉,否则生成的组合里会有一批根本跑不起来。
收束成三步
- 列条件,按取值个数而不是条件个数算规模,互斥的取值合成一个多值条件。
- 铺满、删不可行、合并无关项,再用合计核对。
- 条件少就直接全列,多了改成对覆盖,把关键组合手工补回表里。
最后交代一处不适用。判定表假设结果只取决于“此刻哪些条件同时成立”,与判断的先后顺序无关。如果结果依赖顺序——订单从待支付到已支付、超时取消、退款重入——同一组条件换个顺序就是另一种结果,那时该用状态迁移图 5。