1. 先把话说清楚:这四个方法不是“四选一”,是同一场测试的四个切面
经常有测试同学问我:等价类、边界值、场景法、因果图,面试题背得滚瓜烂熟,一上手写用例就不知道用哪个。还有些人喜欢问“这四种方法哪个最好”,我一般会反问一句:你想用一把螺丝刀还是用一套工具箱?这四个方法压根不是竞争关系,它们是从不同角度看同一份需求——等价类负责把无穷输入切成有限几筐,边界值负责在筐边上看有没有漏,场景法负责把这些筐串成用户真实走的流程,因果图负责处理条件之间那些“说不清道不明”的组合约束。
这篇文章就用一个实际业务模块从头到尾走一遍,我会拿一个在线商城“订单结算优惠”功能当贯穿案例。你跟着我把这份需求拆完,会发现原来用例设计不是靠灵感,而是靠方法组合。
这个案例的需求规则如下,后面所有方法都会围绕它展开:
- 商品总价满199元,系统自动减30元(满减活动);
- 优惠券分三种:新人券(5元无门槛)、品类券(满99减20)、店铺券(满299减50);
- 叠加规则:新人券可以和满减叠加,但不能和品类券同时使用;品类券可以和店铺券叠加,但优惠总额不超过订单金额的50%;每笔订单最多使用2张优惠券;
- 优惠券必须在有效期内,且用户等级达到领券等级才能使用。
是不是看着还挺常见的?但真让你把用例写全,很多人会漏。下面逐个方法过。
2. 等价类划分:先解决“测哪些”,别急着写具体数字
2.1 从需求里提取输入条件和规则
等价类划分的核心动作是“抽象”。你可以把需求里所有能被用户输入、被系统判断的东西都拎出来,这些就是划分对象。
在订单优惠模块里,输入条件至少包括:
- 商品总价(决定是否触发满减);
- 优惠券类型(新人券、品类券、店铺券);
- 优惠券数量(用户选了几张券);
- 优惠券有效期状态(未开始、有效中、已过期);
- 用户等级(是否达到领券等级);
- 优惠券叠加组合方式。
等价类本质上是把“无限多的具体值”归成“有限几类特征”。比如商品总价这一个输入,在系统看来不关心它是100块还是200块,只关心它是否满足“大于等于199”这个判断。你测100和测200,对满减逻辑来说走的是同一条代码路径——这就是为什么要归类。
2.2 有效等价类和无效等价类的划分实例
拿“商品总价”和“优惠券有效期”两个条件举例,划分结果是这样的:
| 输入项 | 有效等价类 | 无效等价类 |
|---|---|---|
| 商品总价 | 满减门槛内金额(≥199) | 不满减金额(<199) |
| 商品总价 | 0元边界(订单异常但可能存在) | 负数、非数字 |
| 优惠券有效期 | 未开始(理论上不可用) | 已过期 |
| 优惠券有效期 | 有效期内 | 日期格式非法 |
| 用户等级 | 达到券种等级要求 | 未达到等级要求 |
这里有个新手容易犯的毛病:只画有效等价类,把无效等价类当“反正都会报错”忽略掉。实际上大量线上故障都出在无效数据的处理上,比如过期优惠券在后端被当成“有效券”参与优惠计算,直接导致用户以错误价格下单。无效等价类不是凑数,它测的是系统的“防守能力”。
还需要注意一点:一个等价类并不总对应一条用例。比如“商品总价 < 199”这是一个无效等价类,但你可以在这个类里选99、198、0各测一遍,因为它们在满减判断之外还可能触发其他逻辑(比如0元订单是否允许提交)。等价类先帮你锁定大类,再谈细节覆盖。
2.3 等价类设计最容易忽略的“伪等价类”
我见过不少测试方案,把“新人券”和“店铺券”划分成两个独立等价类,然后各测一张就收工。这在单券场景下没问题,但需求里写了“叠加使用”,叠加后的行为已经不是“两张单券行为之和”,而是一个新的等价类。
所以等价类划分要特别小心:判断依据不是“长得像”,而是“系统处理路径是否相同”。新人券单独用是一条路径,新人券+满减叠加是一条路径,新人券+品类券(被禁止)又是一条路径。这三条路径对系统来说差异很大,必须分成不同的等价类来覆盖。
等价类的产出通常是一张表,它不需要精确到具体数值,但必须能回答“测试范围覆盖了哪些输入域”这个问题。范围没框好,后面所有方法都白搭。
3. 边界值分析:别只知道0和最大值,矩阵元素的边界也在这套逻辑里
3.1 为什么Bug总爱躲在边界上
做了这么多年测试,我总结出一个规律:程序里60%以上的逻辑错误都出在边界判断上。原因很简单,开发写代码时最常见的判断就是if (amount >= 199)或者if (count <= 2),他们自己也容易搞混“大于”和“大于等于”,更别提循环里的i < length和i <= length差一个单位,可能就直接数组越界。
边界值分析的核心就是针对等价类的边界,取恰好等于、刚好大于、刚好小于边界的值作为测试数据。它和等价类配合使用时,能最大程度覆盖代码中的比较运算符和循环判断。
3.2 优惠模块里的边界值该取哪些数
满199减30,边界值是198、199、200,这个多数人会取。但我要提醒:三个值怎么落到用例上是有讲究的。“满199减30”这个规则里,198和199之差,不只是一个数字之差,而是“触发满减”和“不触发满减”两条路径的分水岭。198测的是不触发路径的上限,199测的是触发路径的下限——这两个值必须同时出现在用例集里,缺一个就有盲区。
再比如“每笔订单最多使用2张优惠券”,边界值是1、2、3。选券数量1是“未达上限”,2是“恰好达到上限”,3是“超过上限”。注意:超过上限的场景必须验证系统是阻止操作还是静默丢弃,需求里如果没有明说,这就是一个要和产品确认的需求缺陷,不是单纯测试用例能兜住的事。
优惠券有效期也是一个典型的边界场景。券的有效期如果是“2024-01-01 00:00:00 到 2024-12-31 23:59:59”,那么需要考虑的边界包括:
- 下单时间恰好等于生效时间(秒级精度);
- 下单时间等于过期前最后一秒;
- 下单时间等于过期时间之后一秒。
有些系统在存储时间精度上不一致,有的用秒,有的用毫秒,这会导致“恰好等于”的判断出现偏差。这种差一秒的问题,最容易在生产环境被用户抓到,因为用户不会精确到秒下单,但定时任务会自动触发。
3.3 顺着“矩阵元素的边界值”这个热词,聊聊多维数据的边界
最近“矩阵元素的边界值”这个词挺热,很多人以为是数学题。但对测试来说,这个词其实点出了一个很实在的场景:被测对象是一张二维表、一个矩阵,甚至一个多维数组时,边界分析不能只盯着“值”的边界,还要盯着“位置”的边界。
我做过一个优惠规则配置平台,后端把“用户等级 × 券类型 × 订单金额区间”存成了一张二维矩阵。这张矩阵本身不大,但测试时发现一个诡异的问题:在矩阵第0行第0列的元素上配置规则后,前端死活不生效,其他位置都正常。查了半天,发现是前端遍历矩阵时用了i <= rows,最后一轮循环越界访问到了下一个内存块,把首地址的数据覆盖了。这就是典型的“矩阵位置边界”问题。
所以当你处理矩阵、数组这类数据结构时,边界值分析要分两层来做:
- 值边界:矩阵元素本身的大小边界,比如优惠金额是否为0、是否为负数、是否超过订单金额的50%;
- 下标边界:遍历时的最小下标0、最大下标rows-1、cols-1,以及越界后的访问行为。
具体到用例层面,至少要有这些:
| 场景 | 测试数据 |
|---|---|
| 矩阵最小值位置 | [0,0]位置配置规则 |
| 矩阵最大值位置 | [rows-1, cols-1]位置配置规则 |
| 单行矩阵 | 1×N矩阵的遍历 |
| 单列矩阵 | N×1矩阵的遍历 |
| 空矩阵 | 0行0列时页面是否报错 |
| 临界值元素 | 矩阵中某个元素恰好等于阈值 |
这里有个反直觉的经验:很多人觉得空矩阵肯定不会测,但实际产品里“还没有配置任何规则”恰恰是上线第一天最常见的状态。一旦空矩阵没有兜底逻辑,用户打开配置页直接白屏,这就是一级故障。
边界值分析的教训永远是同一个:不要想当然。你以为边界是100,实际代码里可能用的是99.99——因为浮点数在计算机里不能精确表示,开发为了规避精度问题会偷偷做偏移。所以如果你在测试中发现边界值行为不符合预期,不妨打开日志看看真实比较逻辑,往往会有惊喜。
4. 场景法:把用例串成一条用户真会走的流程
4.1 场景法从哪里来:基本流和备选流
等价类和边界值最大的问题在于:它们都在“单点”上做文章,而用户真正操作时是一连串动作。用户可能先选商品、再领券、结算、选择优惠、提交订单——任何一步出错,整个流程的行为都可能改变。场景法就是解决“点”到“线”的问题。
场景法来源于事件流分析,它把业务流程抽象成一条基本流(主成功路径)和若干条备选流(分支路径、异常恢复路径)。用例设计的目标就是覆盖基本流、尽可能覆盖备选流。
4.2 订单使用优惠券的完整场景拆解
订单结算这个业务,基本流很清晰:
- 用户选择商品,进入结算页;
- 系统计算商品总价;
- 系统自动匹配满减活动;
- 用户勾选优惠券;
- 系统计算优惠总额并校验叠加规则;
- 用户提交订单;
- 系统生成订单记录。
备选流就多了,我列一部分:
- 商品总价未达到满减门槛;
- 用户没有可用的优惠券;
- 用户勾选新人券后,又勾选品类券(触发“不可叠加”规则);
- 用户同时勾选品类券和店铺券,但优惠总额超过订单金额50%;
- 用户提交订单时优惠券已过期;
- 用户等级不满足优惠券使用条件;
- 用户点击提交后网络异常,订单实际已创建。
把基本流和备选流组合成场景,就得到了场景列表:
| 场景编号 | 场景描述 | 包含事件流 |
|---|---|---|
| S01 | 正常下单,使用满减+新人券 | 基本流 + 满减触发 + 新人券勾选 |
| S02 | 正常下单,未达到满减门槛 | 基本流 + 备选流1 |
| S03 | 新人券与品类券冲突 | 基本流 + 备选流3 |
| S04 | 品类券+店铺券叠加但优惠超过50% | 基本流 + 备选流4 |
| S05 | 下单时券已过期 | 基本流 + 备选流6 |
| S06 | 用户等级不足 | 基本流 + 备选流7 |
你会发现,场景法很自然地把之前等价类里“单点覆盖”的结果,组合成了“整条路径覆盖”。这正是场景法的价值——它用来验证的不是“优惠券能不能用”,而是“用户整个下单过程顺不顺、错时错得准不准”。
4.3 场景粒度控制:别把每个if都写成场景
新手写场景法最容易把粒度切得太细。一个优惠券模块,硬是写出30多个场景,评审时大家越看越晕。我自己的经验是:一条场景必须能对应一个“用户可感知的结果变化”。
比如“用户点击满减标签时标签变灰”这种UI细节,适合用等价类加界面用例去覆盖,不太适合当成场景法的独立场景。但“满减标签变灰且满减金额不计入优惠总额”就是一个合格场景,因为它的结果是用户可以感知到的金额变化。
另一个经验:场景法用例要标注“前置条件”。同一个备选流在不同前置条件下走出来的结果是完全不同的。拿“下单时券已过期”这个场景来说,前置条件是用户已经勾选该券但一直没提交,还是用户刚进入结算页还没勾选——这两种情况,系统一个应该提示“券已过期请重新选择”,另一个应该在下单时自动剔除该券并重算金额。不写清楚前置条件,测试执行时很容易产生争议。
5. 因果图:处理“多个条件组合”的硬骨头
5.1 因果图背后的直觉:条件的组合与约束
等价类、边界值、场景法都有一个共性:它们默认条件之间是相互独立的。但现实项目里,条件往往互相牵制。比如“新人券不能用但品类券能用”“不能同时用但可以叠加满减”——这种话就是因果逻辑,直接用等价类去套很容易漏组合。
因果图法是这么干的:把需求中的“因”(输入条件)和“果”(输出结果)提取出来,用逻辑关系连接起来,再通过约束关系剔除不可能的组合,最后转成判定表生成用例。它解决的是“条件组合爆炸”问题。
5.2 从优惠规则到因果图的建模过程
回到案例,我们把优惠叠加规则中的关键条件和结果拎出来:
- C1:订单金额 ≥ 199;
- C2:用户选择了新人券;
- C3:用户选择了品类券;
- C4:用户选择了店铺券;
- C5:优惠总额 ≤ 订单金额的50%;
- E1:可以使用当前选择组合;
- E2:提示优惠券组合不可用。
用因果图表示这组逻辑,可以得到如下的规则关系:
- C1 与 E1 是“与”关系:满减触发与否不影响券组合是否能用(注意:满减单独判定);
- C2 与 C3 是“排斥”关系:两者不能同时选;
- C2、C3、C4 与 E1/E2 之间是“或/与”组合;
- C5 是 E1 的强制条件:不满足则E2。
这个建模过程本身就会逼着你把需求里模糊的措辞变成精确的逻辑。比如“新人券可以和满减叠加,但不能和品类券同时用”这句话,因果图建模时就必须明确:如果你勾选了新人券又勾选了品类券,系统是直接禁止提交,还是只保留金额更高的那张?需求没说清楚,这就是一个需求缺陷。
5.3 判定表转用例:组合爆炸时怎么“有车可坐”
因果图本身不是直接产出用例的,它的价值是帮你推导出完整的判定表。判定表把条件和动作做成行列矩阵,每一列就是一个规则组合。
拿上面的C1~C5来说,去掉约束条件,全组合是2的5次方=32种。加上约束条件后(C2和C3不能同时为1),有效组合一下降到20种不到。这个数量完全可以支撑用例设计,不需要再盲目扩充。
有人会问:那我有pairwise(成对组合)工具,是不是不需要因果图了?我的看法是:pairwise是算法层面的优化,因果图是理解层面的建模。你连条件之间的约束关系都没理清楚,pairwise生成的组合再均匀也是空中楼阁。实际项目中,我是先手工用因果图把约束关系和强相关规则理清楚,再用工具辅助补充低频组合。
从判定表转用例时,有一条很重要的经验:每条规则都要明确预期结果,不明确的规则本身就是测试发现的需求问题。比如“用户等级不足但优惠券仍在有效期内”这个组合,对应的E结果应该是“不可用且提示等级不足”,如果开发实现成“直接忽略券”,那就是一个提示信息缺陷。
6. 四种方法合体:从一份需求到完整用例清单
6.1 方法选型不是看“哪个高级”,而是看“被测对象长什么样”
我见过一些测试方案,不管什么需求都套场景法用例十几个,然后就说覆盖完成了。这就是没有理解方法的适用边界。做一个方法选型,我一般看四个维度:
| 被测对象特征 | 推荐方法 | 原因 |
|---|---|---|
| 输入域大、数据类型多 | 等价类 | 压缩测试规模,先框范围 |
| 有数值范围、边界条件 | 边界值 | 聚焦最容易出错的临界点 |
| 业务流程明显、步骤多 | 场景法 | 覆盖端到端路径 |
| 条件之间存在多种组合和约束 | 因果图 | 理清逻辑关系,避免遗漏组合 |
这四个维度并不互斥。实际需求往往同时具备几种特征,所以最终方案是“以某一种方法为主线,其他方法做补充”。比如我们这个订单优惠模块,我会以场景法为主线组织用例,因为它的业务流程最核心;在场景内部再用等价类和边界值完善每个步骤的取值;条件组合部分用因果图推导出的判定表补充覆盖。
6.2 完整落地:一个“新人券+品类券不可叠加”需求的用例清单
基于订单优惠模块,我整理一份精简但可直接落地的用例清单,展示四种方法如何汇入一份方案:
| 用例编号 | 设计的出发点 | 前置条件 | 操作步骤 | 预期结果 |
|---|---|---|---|---|
| TC01 | 等价类(有效) | 有满减活动,用户有新人券 | 选商品总价220元,勾选新人券,提交 | 优惠总额=满减30+新人券5=35元,订单实付185元 |
| TC02 | 等价类(无效) | 有满减活动,用户有品类券 | 选商品总价150元,勾选品类券,提交 | 满减不触发,品类券因不满99不可用,提示“未达到使用门槛” |
| TC03 | 边界值 | 有满减活动 | 商品总价199元,不用券,提交 | 满减30元触发,实付169元 |
| TC04 | 边界值 | 有满减活动 | 商品总价198.9元,不用券,提交 | 满减30元不触发,实付198.9元 |
| TC05 | 边界值 | 商品总价120元,品类券满99减20 | 勾选品类券和店铺券,提交 | 店铺券因满299不满足不可用,提示 |
| TC06 | 场景法 | 无 | 结算页同时勾选新人券和品类券 | 系统提示“新人券与品类券不可叠加使用”,且不允许提交 |
| TC07 | 场景法 | 用户已领但券快过期 | 勾选品类券,停留到过期后提交 | 系统自动移除该券并重新计算金额 |
| TC08 | 因果图 | 商品总价400元,品类券+店铺券 | 同时勾选两种券,提交 | 优惠总额150元(≥50%上限200),提示组合不可用 |
| TC09 | 因果图 | 商品总价500元,品类券+店铺券 | 同时勾选两种券,提交 | 优惠总额70元(≤250元),可用,实付430元 |
这份清单只有9条,但四个方法的核心场景都被覆盖了。实际项目里,每条用例还会补充优先级、测试数据、执行环境等信息,这里不展开。
6.3 用例评审时怎么自检:一张查漏清单
用例写完以后,我习惯拿一张自查清单过一遍,比对着覆盖率报告好用得多:
- 每个等价类至少被一条用例覆盖了吗?无效等价类是否真的测了?
- 每个关键边界是否同时取了“边界值-1、边界值、边界值+1”三个点?
- 每一条备选流是否至少出现在一个场景里?异常恢复路径有没有?
- 因果图中的每组约束关系,是否都有“满足约束”和“违反约束”两条用例?
- 用例的预期结果是否足够具体?有没有出现“验证正确”这种说了等于没说的描述?
这个自查流程通常能帮我在评审前拦下一半的漏测。另一半,往往要靠测试环境里的实际执行才能暴露。
7. 实战中踩过的坑:三个“反直觉”教训
7.1 边界值不是比等价类“地位更高”,顺序别搞反
我见过不少测试新手,接到需求上来就找边界,100块、199块、299块的边界测了一堆,结果最基本的功能路径没走通。这不是边界值的问题,是使用顺序出了问题。等价类负责“测全”,边界值负责“测准”,边界值必须建立在等价类划分完整的前提下。先把输入域归类清楚,再用边界值在每个类的边界上补刀,顺序反了,覆盖就是漏的。
7.2 因果图最容易被误解的是“约束”,不是“逻辑”
很多人学因果图时盯着“与、或、非”看,觉得自己懂了。到了实际项目里一用,发现条件之间除了逻辑关系,还有大量约束关系——两个条件不可能同时成立、一个条件成立另一个必须成立、一个条件最多有一个成立。这些约束才是因果图建模时最花时间的地方。
我有个印象很深的经历:某次需求写“每笔订单最多使用2张优惠券,且同一品类券不能重复使用”,测试时我盯着“最多2张”组合,把“同一品类券不能重复”漏了。后来线上出现一个用户用两张同类品类券薅了一笔,才发现开发实现时根本没做“同类不可重复”的判断。原因就是需求里的约束散落在不同段落,没人把它收敛到因果图里。约束关系必须逐条从需求原文中摘出来,不能靠“感觉差不多”。
7.3 覆盖率100%不等于零漏测
度量指标只是参考,不是保险。我见过有些团队把用例覆盖率当作KPI,用例全部执行通过就认为质量达标了。但实际上,覆盖率只能证明“你设计的用例都测了”,不能证明“你没漏设计什么”。
最典型的例子是优惠计算中的精度问题:9.99+19.99这类浮点数相加,在计算机里可能得到29.979999999999998,显示时如果没做精度处理,用户看到的价格就会错一分钱。这种问题,等价类、边界值、场景法可能全都覆盖不到,只有通过“代入真实业务数据”和“对金额计算做专项精度测试”才能发现。
所以我一直强调:方法解决的是“设计效率”问题,真正决定质量的是“测试思维”——永远保持怀疑,永远追问“还有什么情况我没考虑到”。这份怀疑精神,才是等价类、边界值、场景法、因果图背后共通的底层能力。