# 判定表：三个条件铺出八列，为什么砍到五条还够用

讲清判定表怎么按条件取值个数铺满、怎么删不可行组合与合并无关项、怎么用合计核对，以及列数爆炸时成对覆盖能省掉什么、漏掉什么

> 测试用例设计 · 测试分析 · 黑盒测试 · 约 7 分钟 · 10 月 19 日

## 本篇要点

1. 判定表的列数等于各条件取值个数相乘，不是条件个数的幂；互斥的取值（如银卡、金卡）必须合成一个多值条件，否则会排出“既是金卡又是银卡”的假组合。
2. 三个两值条件铺出八列，其中“用券但原价不满 99”的两列与实际数据对不上，属于不可行组合，要整列删掉而不是标成“不测”。
3. 某条件不改变任何动作时写成无关项并可合并两列；本例删两列、并一对，得到五列覆盖六种可行组合。
4. 合并是否合并错了，用合计核对：各列覆盖的原始组合数之和应等于可行组合总数。
5. 判定表把容易漏的叠加缺陷变成必跑的一条用例，例如包邮按原价判断，导致优惠后仅 84 元仍包邮，前三条用例都发现不了。
6. 某个条件在所有列里都不影响动作，说明需求里它没用，或你漏了一条规则——判定表同时是需求检查工具。
7. 覆盖率达到 100% 是按可行列计算的，不是按全部 2 的幂次组合计算。
8. 条件达到五六个、列数上百时改用成对覆盖，只保证任意两个条件的取值组合都出现过；代价是三路及以上才发作的缺陷会漏，资金与权限相关组合要显式补回。
9. 结果依赖先后顺序而非同时成立时，判定表不适用，应改用状态迁移。

---

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

## 先把规则给全

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

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

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

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

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

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

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

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

本例是 $2\times2\times2=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）**：把“覆盖的原始组合数”加起来，应等于删掉不可行列之后的组合总数。这里 $1+1+1+1+2=6$，对得上。合并最容易犯的错，是把动作其实不同的两列并成一列；一旦并错，合计立刻对不上。

顺带说清口径：这类表的 100% 覆盖按**可行列**算，不是按 $2^n$ 算 [5]。

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

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

对着规则算四条，用来验证表本身对不对：

- **T3（金卡、不用券、原价 120）**：$120\times0.9=108$，原价满 99 包邮，应付 108。
- **T4（金卡、不用券、原价 98）**：$98\times0.9=88.20$，不满 99 收 8 元运费，应付 96.20。
- **T2（银卡、不用券、原价 99）**：$99\times0.95=94.05$，包邮，应付 94.05。
- **T5（任意等级、用券、原价 99）**：$99-15=84$，包邮，应付 84。

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

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

## 怎么判断这张表没写漏

没有别人帮你判卷，用这四条自查：

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

## 条件一多，列数会失控

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

$$3\times2\times2\times2\times3\times2=144$$

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

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

用之前要知道三条边界：

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

## 收束成三步

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

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

## 术语表

- 判定表：把“哪些条件取什么值 → 系统做哪些动作”逐列写全的表格，一列是一条判断规则，也是一组必须覆盖的逻辑。
- 条件桩与动作桩：表格左上为条件桩（所有左右结果的因素），左下为动作桩（系统可能做出的所有响应）。
- 无关项（–）：表示这一条件在当前列取什么值都不影响动作，因此可把动作相同的多列合并成一列。
- 不可行组合：条件之间互相矛盾、现实中造不出数据的组合，要从表中删掉，而不是当成“不用测”留着。
- 成对覆盖（pairwise）：只要求任意两个条件的取值组合都至少出现过一次，用远少于全组合的条数压住组合爆炸。
- 交互失效：单个条件各取正常值都无事，只有两个以上条件取到特定组合才发作的缺陷，也是成对覆盖针对的目标。

## 来源

1. [TU Delft 软件测试课程章节：判定表结构、无关项与默认列](https://mordeky.github.io/SQAT/chapters/testing-techniques/model-based-testing.html)
2. [SQRBOK 手册：判定表的八步构造法与合并后的 checksum 核对做法](https://sqrbok.github.io/content/verif/domain/decision-tables.html)
3. [NIST《Combinatorial Testing: Theory and Practice》— 67%／93%／98% 检出率与交互失效分布](https://tsapps.nist.gov/publication/get_pdf.cfm?pub_id=918448)
4. [NIST SP 800-142《Practical Combinatorial Testing》— 成对覆盖的前提与局限](https://doi.org/10.6028/nist.sp.800-142)
5. [Decision Table vs State Transition Testing — 覆盖率按可行列计算及两种方法的适用分界](https://sqadojo.com/learning-hub/compare/decision-table-vs-state-transition-testing)

---

原文：https://pangzhengboyin.com/articles/decision-table-combination-explosion-95bdc655

> **庞征博引** · 想学的，慢慢都会
>
> 庞征博引是把想学的东西写成连载的 AI 学习工具。说出想学什么，它会先了解你的基础，再把主题写成一篇篇 5–10 分钟能读完的文章；边读边问，接下来学什么跟着你走。这篇就是这样写出来的。
>
> 开始你自己的连载 → https://pangzhengboyin.com
