做测试这些年如果要我选一道“练手性价比最高”的黑盒测试案例自动售货机问题绝对排前三。它看起来简单无非是投币、选饮料、出货、找零但真上手去做测试设计你会发现这里面的坑比想象中多得多。尤其是用因果图法来拆这个场景既能把黑盒测试的核心思想练透又能把因果图的符号、约束、判定表转换整个流程完整跑一遍。这篇文章我就拿这个经典案例从头到尾拆一遍因果图法的完整操作包含我怎么分析原因结果、怎么处理约束、怎么把因果图转成判定表、最后怎么生成可执行的测试用例。整个过程会聊到很多实操细节和踩坑经验适合刚入门测试、或者想系统梳理因果图法的朋友当参考。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通过“与”的关系汇入E1M2与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种理论组合筛了一遍去掉不可能出现的d10,d21组合去掉c4和c5同时为1的互斥组合剩下有效规则明显减少。这个筛选动作也是在模拟真实项目中“条件化简”的过程。4.2 判定表长什么样每一列我怎么解读我筛出来的有效规则主要有这些对应关系列出来会清晰很多规则编号d1金额≥2d2金额≥3c4按可乐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条件是d11、d21、c41也就是“金额≥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条避坑建议都是实打实踩出来的经验画图之前先确认需求边界一次交易能不能买多瓶操作顺序是先投币还是先按按钮项目里没有明确写出来的全部要到评审会上确认不要自己拍脑袋。原因只放“用户可操作”的动作系统内部计算出的状态放到中间节点。这条规则能帮你避免80%的因果图画错问题。结果要区分“系统最终动作”和“中间动作”。出货、退币是最终动作找零计算过程不是。五种约束关系不是画图仪式每一个约束都必须转化成具体的测试场景。画完图后逐个检查看它对应的场景是否出现在最终用例里。判定表转用例时必须把“条件状态”还原成“具体输入”。规则写着“金额大于等于3元”你要自问一句3元可以有哪些投币组合穷举一遍再决定写几条用例。找零金额的计算是自动售货机最容易出错的地方尤其要覆盖“正好金额”和“超出金额”的边界。2元买可乐不找零2元买雪碧失败退币这两个边界不能只测一个。退币路径要单独梳理。按按钮失败退币、购买成功后找零、没按按钮取消退币这三条路径的金额行为完全不同必须各写用例。判定表里无效组合要显式标记并说明原因不要直接删除。团队评审时你能解释“这一行为什么不存在”比让同事自己去猜要省时间得多。用因果图法产出的测试用例要回灌到需求评审里。你在分析过程中发现的需求模糊点往往是产品逻辑的真正风险记录成评审问题比默默假设更有价值。不要把因果图法当成银弹。条件数量超过5到8个时先用等价类划分做条件剪枝再画因果图或者直接转判定表效率和覆盖率才能兼顾。6. 一些更深入的思考这个案例还能怎么延伸自动售货机问题如果只做到“投币-选择-出货”这个深度其实还没完全发挥因果图法的价值。我前面已经提到找零但找零的细节还可以继续展开。比如机器里1元硬币库存不足时怎么办这涉及系统状态和资源约束已经超出了“用户输入条件”的范畴进入状态图测试法的领域了。这时候因果图法就会力不从心需要引入状态图或场景法来做补充。所以你在学习因果图法时一定要清楚它的能力边界。另一个可以延伸的方向是超时处理。用户投币后30秒内没有按按钮机器是自动退币还是保持等待如果投币后按钮卡住系统超时时间到了怎么处理这些都属于时间约束因果图法表达不了时间维度只能靠场景法设计正常流和异常流的用例。我在做测试设计评审时经常会把“自动售货机”当引子问团队一个问题如果用户投了10元按下可乐按钮出货后机器应该找8元但这时候机器里只剩5枚1元硬币系统怎么处理这个问题一问出来大家马上就会意识到简单的因果图分析只覆盖了逻辑层的输出但物理设备层的异常状态完全没考虑。真正的测试设计一定是多种方法组合使用的因果图法负责把组合逻辑理清边界值分析和状态图法负责把边界情况和状态流转补全。这个案例到了这一步才算真正把黑盒测试的思维方式练透。我自己在面试测试岗位时也很喜欢拿自动售货机问题来考察候选人的逻辑拆解能力。能把原因、结果、约束、判定表讲清楚的人通常写测试用例的思维也会很严密。反过来一上来就凭感觉写用例的人往往在这个问题上会露出很多漏洞。所以如果你正在准备测试相关的工作面试或者刚入行想找一道题系统练习黑盒测试方法自动售货机这个案例绝对值得多刷几遍。