☰
自动售货机测试用例设计:因果图法从入门到实战
2026/10/2 8:12:20 网站建设 项目流程

做测试这些年,如果要我选一道“练手性价比最高”的黑盒测试案例,自动售货机问题绝对排前三。它看起来简单,无非是投币、选饮料、出货、找零,但真上手去做测试设计,你会发现这里面的坑比想象中多得多。尤其是用因果图法来拆这个场景,既能把黑盒测试的核心思想练透,又能把因果图的符号、约束、判定表转换整个流程完整跑一遍。这篇文章我就拿这个经典案例,从头到尾拆一遍因果图法的完整操作,包含我怎么分析原因结果、怎么处理约束、怎么把因果图转成判定表、最后怎么生成可执行的测试用例。整个过程会聊到很多实操细节和踩坑经验,适合刚入门测试、或者想系统梳理因果图法的朋友当参考。

1. 这个经典案例,到底在考什么

1.1 一个看似平淡无奇的自动售货机需求

先看需求描述,这是最典型的版本:

  • 自动售货机售卖两种饮料:可乐2元,雪碧3元。
  • 机器支持投入1元、5元、10元三种硬币。
  • 用户先投币,投币累计金额达到或超过商品价格后,再按下对应按钮,机器出货。
  • 如果投币金额不足,按下按钮不出货,机器退币。
  • 如果投币金额大于所购商品价格,机器需要找零。

这个需求看起来逻辑清楚,没有歧义。但如果你真的拿去做测试,马上就会遇到几个问题:投10元买2元的可乐,找零8元,这8元用几个1元硬币找?如果用户投了两个5元,一共10元,买2元可乐,机器一次找8枚1元硬币,这在现实机器里可能直接卡币。还有,用户先按按钮再投币,系统该怎么处理?投币过程中硬币卡住怎么办?

这些问题的共性在于:需求文档只描述了“正常路径”,对输入条件之间的组合关系、约束关系、异常分支几乎没有任何定义。而因果图法干的恰恰就是这个活儿,它逼你把每条输入条件拆出来,把条件之间的“与、或、非、互斥、依赖”关系画清楚,再逐条推演输出结果。用在这个案例上,刚好能把你想不到的测试场景全部逼出来。

1.2 黑盒测试方法那么多,为什么因果图更适合这个场景

黑盒测试的常用方法有等价类划分、边界值分析、判定表、因果图、正交实验、场景法等等。很多新手会有疑问:自动售货机这个需求,用等价类加边界值不也能测吗?

能测,但会测得很痛苦。原因很简单:这个场景的输出结果并不只依赖某一个输入条件,而是依赖多个条件的组合。比如“是否出货”这件事,取决于“投币金额是否足够”和“是否按下了对应按钮”两个条件共同作用;“是否找零”又取决于“投币金额”和“商品价格”的大小关系。单个条件的等价类划分根本覆盖不了这种组合逻辑。

还有一点很关键,这个需求里存在明确的约束关系。用户不可能同时按下可乐按钮和雪碧按钮,这就是典型的互斥约束。没有约束概念的等价类分析,很难发现这类条件之间的限制。而因果图法本身就是从因到果的推导逻辑,中间穿插约束建模,最后还可以转化成判定表做到全组合覆盖,这是它在这个场景下最大的优势。

我在实际带新人时,会刻意选择这个案例来训练他们的分析能力,因为它的复杂程度刚好合适:输入条件不算太多,逻辑链条不复杂,但已经足够暴露出“不梳理组合就直接写用例”带来的漏测问题。

2. 因果图法的底层逻辑,先搞懂这五类约束和三个符号

2.1 因果图法到底解决什么问题

因果图法的核心思想只有一句话:从输入条件出发,分析输入条件不同组合下系统的输出结果,从而设计出覆盖全面的测试用例。这句话听起来简单,实际执行时要做的是把自然语言写的需求,翻译成一张包含“原因”、“中间节点”、“结果”的逻辑图,再把这张图转成可以穷举组合的判定表,最后从判定表里生成用例。

这里要特别注意“原因”和“结果”的定义。原因是用户可操作的、系统的输入条件,比如“投入1元硬币”、“按下可乐按钮”。结果是系统对用户操作的反馈,比如“售出可乐”、“退币”。中间节点则是我们在推导过程中引入的中间状态,比如“投币累计金额大于等于2元”,它是由多个原因组合推出来的,但它本身又不是最终输出结果。

我第一次画因果图的时候,最容易犯的错就是把中间节点当成原因直接画上去,比如直接在原因里写“投币金额足够”。这虽然能让图变简单,但会丢掉“金额是由哪些硬币组成的”这层信息,后面写测试用例时就会漏掉不同的投币组合方式。正确的做法是,先把原子输入条件拆出来,再通过中间节点去表达组合后的状态。

2.2 五种约束关系,自动售货机里个个都用得上

因果图里的输入条件之间并不是彼此完全独立的,很多业务逻辑本身就包含条件之间的限制。因果图法用五种约束符号来表达这些限制,这也是新手最觉得抽象的部分,我一个个说。

  • E(互斥,Exclusive):两个条件最多成立一个,不能同时成立。自动售货机里,“按下可乐按钮”和“按下雪碧按钮”就是典型的互斥。正常人一次只能按一个按钮,业务也会限制不能同时选两种饮料。
  • I(包含,Inclusive):至少有一个条件成立,但可以多个同时成立。比如“投入1元硬币”、“投入5元硬币”、“投入10元硬币”这三个条件,至少有一个成立才能继续交易,但用户可以同时投入多枚不同面额硬币。
  • O(唯一,One and only one):多个条件中必须有一个且只能有一个成立。比如售货机饮料类型的选择,可以设计成“必须且只能选一种”,这比E约束更强,E只是“不能同时选”,O还要求“必须选一个”。
  • R(要求,Requires):一个条件成立时,另一个条件必须也成立。比如“按下可乐按钮”这个条件成立,前提是“投币金额大于等于2元”这个条件必须成立,这就是一种要求关系。
  • M(屏蔽,Mask):一个条件成立时,另一个条件必须不成立。比如“机器处于故障状态”成立时,“可以正常售货”这个条件就被屏蔽掉了,不再影响输出。

很多人会问,这些约束关系我在画图时怎么确定?答案只有一个:回头逐字逐句核对需求,需求没写的部分去找产品经理确认,而不是自己想当然。自动售货机这个案例里,需求文档只写了正常流程,没写“先按按钮后投币怎么办”,这就是一个需求缺陷,你应该把它记录下来,作为评审问题提出,而不是自己猜一个逻辑写进用例。

2.3 因果图到判定表,这一步为什么省不掉

因果图画出来之后,直接对着图写用例可以吗?可以做,但很容易漏。因为因果图是一种逻辑关系图,人的眼睛在图上找组合时,不容易判断“我是否已经覆盖了所有可能的原因组合”。判定表的作用就是把“所有原因的组合”和“对应结果”以矩阵形式罗列出来,把逻辑问题转换成查表问题。

判定表的行是原因和结果,列是每一种条件组合。理论上,如果有n个原因,组合数就是2的n次方。自动售货机案例中,如果拆出5个原因,全组合是32种,还可以接受。但这只是理论值,如果原因有10个,2的10次方就是1024种,判定表会膨胀到没法看。这也是因果图法在实际项目中最常被吐槽的点——条件多的时候根本不现实。

我的习惯是:因果图法重点用在“核心业务逻辑梳理”阶段,条件超过5到8个时,先做条件化简,再用判定表做局部覆盖,而不是一股脑全组合。自动售货机这个案例刚好把完整的流程走一遍,也好让大家体会到判定表在中等复杂度逻辑下的可用性。

3. 自动售货机需求拆解实战:从原因到结果一步步来

3.1 先确认边界:一次完整交易到底包括什么

动手之前,必须先把“一次交易”的边界定清楚。这个边界不定,后面所有拆分都是空中楼阁。我这里定义的自动售货机场景是:用户一次购买流程只买一瓶饮料,操作顺序是先投币、再按按钮。一次流程中允许投入多枚硬币,但最终只允许按下一种饮料的按钮。

这个定义本身就是我在实际评审中一定会和产品确认的内容。因为现实售货机还有“一次买多瓶”“先按按钮后投币”“投币后不按按钮直接退币”等交互逻辑。如果需求文档没有明确,测试设计可以有两种处理方式:一是对所有可能的操作顺序都设计用例;二是把未定义的逻辑列为问题项,推动需求补全。自动售货机这个经典题目,多数资料默认走“先投币后选择”的路径,但我们在实战中一定要保留对未定义逻辑的敏感度,这才是因果图法训练的真正价值。

边界确认后,我来拆“原因”。“原因”必须是用户的操作动作,必须是一个布尔条件,是“是/否”这种能判断真假的状态。按照这个标准,自动售货机的原因可以拆成这些:

  • C1:投入了1元硬币
  • C2:投入了5元硬币
  • C3:投入了10元硬币
  • C4:按下了可乐按钮
  • C5:按下了雪碧按钮

这里注意,C1、C2、C3并不是“投入的硬币面额”,而是“是否投入了该面额的硬币”。一个用户可能在一次交易里投了1元硬币加5元硬币,那C1和C2同时为真。这样拆分是为了后续能覆盖不同硬币组合的场景。投币总额这个信息,我用中间节点来表达。

3.2 中间节点怎么定义,决定了这张图好不好用

中间节点是我自己推导出来的状态,它不属于用户操作,而是系统根据用户操作计算出的结果。自动售货机案例里最重要的中间节点就是投币总额和饮料价格的关系。

  • M1:累计投币金额大于等于2元
  • M2:累计投币金额大于等于3元

为什么M1和M2要分开定义?因为2元是可乐的价格,3元是雪碧的价格。只有M1成立时,系统才可能售出可乐;只有M2成立时,系统才可能售出雪碧。M2成立时M1必然成立,因为金额大于等于3元一定大于等于2元,这种包含关系在判定表化简时会用到。

接下来定义“结果”。“结果”是系统的输出反馈,自动售货机系统的结果有三个:

  • E1:售出可乐
  • E2:售出雪碧
  • E3:退币

有些资料还会把“不出货”作为一种结果,但“不出货”其实可以通过E1和E2都为假来表示,不必单独列出来,否则判定表会膨胀。同样的,“找零”和“退币”在经典题目里可以合并处理,因为它们的本质都是“系统退还硬币”。如果你要测试的场景要求区分“找零金额”和“原始投币金额”,那就要把退币和找零拆成两个结果,实际工作中完全取决于业务关注点。

3.3 梳理因果关系,把逻辑画成图

现在把所有逻辑关系串起来。核心的因果关系是:

  • 售出可乐:M1成立 且 C4成立 → E1成立
  • 售出雪碧:M2成立 且 C5成立 → E2成立
  • 退币:投币金额不足时按按钮,或者没有按下任何按钮但投币了,或者购买成功后的找零

退币这个结果需要特别讨论。在实际场景中,退币有三种完全不同的触发条件,如果混在一起画,图会乱,判定表也会乱。我更推荐的做法是把退币拆成三种中间结果,再汇总成最终的退币动作:

  • M3:按下了饮料按钮但金额不足,触发“交易失败退币”
  • M4:购买成功但投币金额多于商品价格,触发“找零”
  • M5:投币后未按任何按钮,触发“操作取消退币”

M3、M4、M5任何一个成立,最终都会导致系统退币,也就是E3。这样拆的好处是,测试用例可以区分不同的退币场景,而不是笼统地验证“最后退钱了”。我在实际项目里发现,很多线上问题恰恰就是对退币场景区分不清导致的,比如“找零”和“操作取消退币”的金额计算方式完全不同。

画因果图时,C1、C2、C3通过“或”的关系汇入M1和M2,然后M1与C4通过“与”的关系汇入E1,M2与C5通过“与”的关系汇入E2。同时C4和C5之间是E约束(互斥),C1、C2、C3之间是I约束(至少有一个成立,这里并不严格强制,但因为没投币系统不会进入出货逻辑,实际可以理解成I约束)。这些约束关系要在图里用专门的符号标出来,否则后面判定表可能生成大量无意义组合。

4. 从因果图到判定表,完整转换过程全记录

4.1 先化简条件,把原始原因组合改成状态组合

直接拿C1到C5五个原因做判定表,理论上要32列,虽然数量可以接受,但很多列是无效或者重复的。比如C1和C2、C3都是假,表示用户一分钱都没投,这时按下饮料按钮根本不可能出货;再比如C4和C5同时为真,这种组合在业务上被E约束直接排除。

所以我的处理方式是分两步。第一步,先把“投币金额”这个信息从三种硬币的布尔组合中抽象出来。既然M1和M2才是真正影响结果的中间节点,我直接把M1和M2当成判定表的条件使用,把C1、C2、C3的组合先合并掉。这对新手来说可能会有点绕,因为判定表的条件列里出现的不再是原始原因,而是中间节点。但这样判定表的列数直接从32降到16,而且更贴合业务的判断逻辑。

M1和M2有个天然约束:M2为真时M1必然为真,所以M1=假、M2=真这种组合在物理上不存在。判定表里这个组合必须被排除,否则就是无效规则。这一步至关重要,很多人做完判定表拿去写用例,结果用例里出现“投币金额小于2元但大于等于3元”这种荒谬场景,就是因为没做约束检查。

化简之后,判定表的条件有四个:

  • d1:累计投币金额大于等于2元
  • d2:累计投币金额大于等于3元
  • c4:按下了可乐按钮
  • c5:按下了雪碧按钮

再配合E约束和M约束,我把16种理论组合筛了一遍,去掉不可能出现的d1=0,d2=1组合,去掉c4和c5同时为1的互斥组合,剩下有效规则明显减少。这个筛选动作也是在模拟真实项目中“条件化简”的过程。

4.2 判定表长什么样,每一列我怎么解读

我筛出来的有效规则主要有这些,对应关系列出来会清晰很多:

规则编号d1(金额≥2)d2(金额≥3)c4(按可乐)c5(按雪碧)E1(出可乐)E2(出雪碧)E3(退币)场景说明
10010001金额不足,按可乐,交易失败退币
20001001金额不足,按雪碧,交易失败退币
31010100金额2元,正好买可乐
41001001金额2元,按雪碧,钱不够,退币
51110101金额≥3元,按可乐,出货并找零
61101011金额≥3元,按雪碧,出货并找零
71100001投币后未按按钮,取消退币

注意第5条和第6条都出现了“退币”,这里的退币是“找零”语义,比如投了5元买3元雪碧,系统出雪碧并退2元。第3条没有退币,因为2元买可乐是正好,一分不多一分不少。

这7条规则看着不多,但它们是从16种组合中严格筛选出来的。如果直接把所有组合都写进去,不仅会多出大量无效测试用例,还会稀释真正有价值的场景。我在实际写用例时会经常提醒自己,判定表是为了找有效组合,不是为了凑列数。

4.3 从判定表到测试用例,还要补上原始输入细节

判定表里的规则对应的是“条件状态”,但写具体的测试用例时,还得把“状态”还原成具体的操作数据。比如规则5,条件是d1=1、d2=1、c4=1,也就是“金额≥3元且按下可乐按钮”。那用例怎么投币?可以投3个1元,也可以投1个5元,还可以投1个5元加1个1元。这些投币方式就是C1、C2、C3的组合细节,它们在确定d1、d2状态时已经合并了,但落到用例时必须还原。

我给每条有效规则生成用例时,会刻意覆盖不同的投币组合,而不是只挑一种。比如规则5,我会拆成这几条实际用例:

  • 投1元硬币三枚,投币总额3元,按下可乐按钮,预期结果:出可乐,找零1元。
  • 投5元硬币一枚,投币总额5元,按下可乐按钮,预期结果:出可乐,找零3元。
  • 投10元硬币一枚,投币总额10元,按下可乐按钮,预期结果:出可乐,找零8元。
  • 投1元硬币两枚加5元硬币一枚,投币总额7元,按下可乐按钮,预期结果:出可乐,找零5元。

规则6对应雪碧,同理可以用不同投币组合来覆盖。这个过程清楚地展示了因果图法的完整链条:原因组合先归结为中间状态,再由状态生成判定表规则,最后测试用例又回到具体输入。每一步都是值得验证的,没有一步是可以跳过的。

5. 实操中那些容易踩的坑,附避坑清单

5.1 原因和中间节点混淆,是新手第一个大坑

我见过太多人画自动售货机的因果图,直接上来就把“投币金额足够”当成原因。这样画看着简洁,好像也没毛病,但到写用例时就卡住了:“投币金额足够”到底是多少钱?是2元足够还是3元足够?可乐和雪碧价格不一样,怎么统一表达?

根源就是没搞清原因必须是原子输入。原因就是用户的一次操作,硬币一枚一枚投,按钮一个一个按,“投币金额足够”是系统根据用户操作计算出来的内部状态,不是用户操作本身。正确做法是像上面那样,把投币拆成C1、C2、C3这种原子条件,再通过M1、M2中间节点去表达“金额足够”这个状态。这样拆出来的用例,投币组合边界非常清楚,绝不会出现漏掉某一种面额组合的情况。

5.2 约束条件一笔带过,测试用例直接漏场景

我培训时专门做过实验,让两组人分别用因果图法设计自动售货机的测试用例。一组重点强调约束关系,另一组只是提了一句“别忘了约束”。结果没强调约束的那组,几乎全都漏掉了“投币后不按按钮直接退币”这个场景,或者漏掉了“按了按钮但金额不足要退币”的场景。

为什么?因为人的思维惯性会顺着“正常购买路径”走,投币、按按钮、出货,很少主动去思考“投了币但是不按按钮怎么办”。但约束关系恰恰就在逼你思考这些。C4和C5的E约束,逼你回答“两个按钮都按了会发生什么”;C1、C2、C3的I约束,逼你回答“一个硬币都不投行不行”;M1和M2之间的逻辑包含关系,逼你思考“金额大于等于3元时,买可乐要不要找零”。每个约束背后都是一个真实的测试场景。跳过约束,就等于跳过了测试设计里最值钱的部分。

5.3 因果图法不是万能药,条件太多时懂得做减法

自动售货机这个案例,原因只有5个,理论上32种组合,做完化简后有效规则7条,这个过程非常理想化。但真实项目里,一个接口可能有十几个输入参数,每个参数还有多个取值,直接套因果图,判定表能膨胀到几千列。这是因果图法被很多团队放弃的原因,但它不是方法本身的问题,而是用法的问题。

我的经验是,因果图法最适合用在“核心逻辑链路”上,比如支付、订单状态流转这种逻辑复杂、组合多、出错后果严重的模块。先把这些模块的因果图画清楚,再配合等价类划分和边界值分析处理每个参数的取值细节。至于参数特别多、组合爆炸的场景,该用Pairwise(成对组合测试)就用Pairwise,工具能生成更少的组合数。因果图的角色更多是“需求逻辑梳理器”,而不是“用例生成器”,这个定位想明白,就不会觉得它鸡肋了。

5.4 这10条避坑建议,都是实打实踩出来的经验

  1. 画图之前先确认需求边界:一次交易能不能买多瓶?操作顺序是先投币还是先按按钮?项目里没有明确写出来的,全部要到评审会上确认,不要自己拍脑袋。

  2. 原因只放“用户可操作”的动作,系统内部计算出的状态放到中间节点。这条规则能帮你避免80%的因果图画错问题。

  3. 结果要区分“系统最终动作”和“中间动作”。出货、退币是最终动作,找零计算过程不是。

  4. 五种约束关系不是画图仪式,每一个约束都必须转化成具体的测试场景。画完图后逐个检查,看它对应的场景是否出现在最终用例里。

  5. 判定表转用例时,必须把“条件状态”还原成“具体输入”。规则写着“金额大于等于3元”,你要自问一句:3元可以有哪些投币组合?穷举一遍再决定写几条用例。

  6. 找零金额的计算是自动售货机最容易出错的地方,尤其要覆盖“正好金额”和“超出金额”的边界。2元买可乐不找零,2元买雪碧失败退币,这两个边界不能只测一个。

  7. 退币路径要单独梳理。按按钮失败退币、购买成功后找零、没按按钮取消退币,这三条路径的金额行为完全不同,必须各写用例。

  8. 判定表里无效组合要显式标记并说明原因,不要直接删除。团队评审时,你能解释“这一行为什么不存在”,比让同事自己去猜要省时间得多。

  9. 用因果图法产出的测试用例,要回灌到需求评审里。你在分析过程中发现的需求模糊点,往往是产品逻辑的真正风险,记录成评审问题比默默假设更有价值。

  10. 不要把因果图法当成银弹。条件数量超过5到8个时,先用等价类划分做条件剪枝,再画因果图或者直接转判定表,效率和覆盖率才能兼顾。

6. 一些更深入的思考:这个案例还能怎么延伸

自动售货机问题如果只做到“投币-选择-出货”这个深度,其实还没完全发挥因果图法的价值。我前面已经提到找零,但找零的细节还可以继续展开。比如机器里1元硬币库存不足时怎么办?这涉及系统状态和资源约束,已经超出了“用户输入条件”的范畴,进入状态图测试法的领域了。这时候因果图法就会力不从心,需要引入状态图或场景法来做补充。所以你在学习因果图法时,一定要清楚它的能力边界。

另一个可以延伸的方向是超时处理。用户投币后30秒内没有按按钮,机器是自动退币还是保持等待?如果投币后按钮卡住,系统超时时间到了怎么处理?这些都属于时间约束,因果图法表达不了时间维度,只能靠场景法设计正常流和异常流的用例。

我在做测试设计评审时,经常会把“自动售货机”当引子,问团队一个问题:如果用户投了10元,按下可乐按钮,出货后机器应该找8元,但这时候机器里只剩5枚1元硬币,系统怎么处理?这个问题一问出来,大家马上就会意识到,简单的因果图分析只覆盖了逻辑层的输出,但物理设备层的异常状态完全没考虑。真正的测试设计,一定是多种方法组合使用的,因果图法负责把组合逻辑理清,边界值分析和状态图法负责把边界情况和状态流转补全。

这个案例到了这一步,才算真正把黑盒测试的思维方式练透。我自己在面试测试岗位时,也很喜欢拿自动售货机问题来考察候选人的逻辑拆解能力。能把原因、结果、约束、判定表讲清楚的人,通常写测试用例的思维也会很严密。反过来,一上来就凭感觉写用例的人,往往在这个问题上会露出很多漏洞。所以如果你正在准备测试相关的工作面试,或者刚入行想找一道题系统练习黑盒测试方法,自动售货机这个案例绝对值得多刷几遍。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询