实战:从建模到用例生成,解决漏测难题)
开篇先抛一个问题你负责的那条核心业务链路测试用例真的把所有该走的路径都走过了吗我带过的测试小组里多数人思考几秒后给出的答案是应该吧。但应该这两个字恰恰是漏测的起点。因为手工设计用例时写的人和评审的人用的是同一套业务认知漏掉的盲区会被大家集体漏掉。我后来认真去解决这个问题用的就是基于模型的测试Model-Based TestingMBT。这一篇我想系统讲讲这条用机器设计测试用例的路它到底解决什么问题、模型怎么建、用例怎么生成、落地会踩哪些坑以及怎样在一个团队里小步跑通。如果你正被用例覆盖率上不去、回归用例维护成本高、漏测总是事后才发现这些问题困扰这篇内容可以给你一个明确的方向把系统行为抽象成模型让机器基于模型遍历生成测试用例。它不会替代你但能把从头想用例这个重体力活接过去。1. 为什么要把用例设计交给机器手工用例的三个死穴1.1 手工用例真正的问题不是写得不够多很多人以为漏测是因为用例数量太少多写几条就行了。我在实践中发现问题远不是数量能解决的。用手工方式设计测试用例天然带着三条结构性缺陷。第一条是路径组合盲区。只要系统里有几个分支点全组合就会指数级膨胀。拿一个支付接口举例需要覆盖风控拦截、优惠券过期、余额不足、超时重试这四个独立因素的两两组合光业务主流程就有十几种可能。人脑面对这种组合本能的做法是挑几条感觉容易出错的路径然后默认其他路径都安全。可软件测试的残酷之处在于恰恰是那些没被挑中的组合会出事。第二条是路径偏好。几乎每个用例设计者都有自己的偏好有人喜欢走主流程把异常分支随便补几条有人喜欢堆异常场景把必填项为空这种用例写上几十条还有人迷信边界值对组合路径基本不设防。这种偏好会导致整个测试用例集系统性偏向某类场景。我在代码评审时见过一个功能模块所有用例都覆盖了正常流程错误提示唯独没人测操作中途另一用户修改了同一份数据这种并发冲突场景结果上线第一周就出了脏数据。第三条是回归维护的不可持续。手工用例和需求文档是两套东西需求一变用例要逐条对照着改。一个模块如果有两百条用例需求变动十次后用例和实际行为往往已经对不上了。大家都说是维护用例实际上很多团队是在猜用例猜哪条还有效、哪条已经失效、哪条描述的是旧逻辑。这个成本长期看非常可观。这三条缺陷结合在一起会让一个团队的测试工作陷入一个怪圈用例越来越多覆盖率感觉越来越好但漏测依旧发生并且没人能说清楚漏点在哪。这个怪圈靠堆人是破不掉的必须换一种设计用例的输入方式。1.2 基于模型的测试到底做了什么、没做什么基于模型的测试核心思路可以概括成一句话人在建模机器遍历。它要求测试人员先用一种结构化的方式描述被测系统的预期行为——通常是一张状态机、一张决策表或者一套规则集——然后由算法基于这个模型去生成测试用例。这里要澄清一个常见误解MBT和现在很热的AI生成用例不是一回事。AI生成用例本质上是概率预测它根据历史数据猜哪些用例像有效的用例MBT则是确定性算法它从明确的行为模型出发算出哪些路径还没走到、哪些状态还没达到、哪些规则还没触发。MBT不依赖历史包袱模型里写的每一个状态转换都是明确可校验的。MBT实际做了三件事。第一把系统的行为结构化形成一份可以被计算机理解的行为地图。第二用图遍历、路径搜索等手段把这份地图里所有有价值的路径自动生成出来。第三显式地把覆盖标准摆到台面上——状态覆盖率、迁移覆盖率、决策规则覆盖率都可以量化。这三个能力正好补上手工用例的三个死穴机器遍历没有路径偏好组合路径是它的天然优势覆盖率计算也远比我觉得测够了靠谱。当然MBT也不是万能的。它不替代需求分析模型本身还是人建的业务理解错了模型也会错。它也不自动产生测试断言虽然能自动跑路径但每个状态应该满足什么条件仍然需要人来定义。它更不是全自动测试的银弹后面我会专门讲它适合什么样的模块。2. 模型不是一张图五种常见模型形态与选型思路2.1 状态机模型事件驱动系统的基础在MBT的世界里最常见的模型形态是有限状态机。状态、事件、迁移三样东西支撑起一整类系统的建模需求。状态代表系统在某个时刻可观察的稳定情况事件是触发状态改变的动作迁移则是在某个状态下发生某个事件后系统进入另一个状态的规则。举例来说一个订单模块可以抽象成这些状态待支付、支付中、已支付、已发货、已完成、已取消、退款中、已退款。事件则是提交订单、支付成功回调、支付超时、用户取消、商家发货、用户确认收货、用户申请退款、退款审核通过。把状态和事件按逻辑连起来一张订单状态机就成型了。我习惯把状态机模型理解为业务层面的红绿灯系统每个状态就是一组灯色每个事件代表一次交通流迁移规则规定灯色之间能否直接切换。红绿灯不可能从红灯直接切到黄灯业务模型也是一样状态之间必须有合法迁移路径否则就是建模错误。建模时如果发现两个状态之间的迁移关系画不出来往往说明业务设计本身有漏洞这个发现本身就是MBT的附加值。状态机模型最适合事件驱动的业务系统订单、审批流、工单、任务调度、用户生命周期。它有非常成熟的图遍历算法支撑是MBT落地最顺手的形态。入门MBT建议先从状态机开始。2.2 决策表模型适合条件组合多的业务规则状态机擅长描述按时间推进的状态流转但有一类场景它表现得不好条件组合爆炸。比如促销活动系统里折扣金额取决于用户等级、订单金额、是否会员日、是否新客、是否有优惠券这五个条件。如果把每个条件的不同取值都变成状态模型会迅速膨胀成一张没法看的网。这类场景应该用决策表模型。决策表的思路是把所有条件列成列所有规则列成行每个规则指定一组条件取值以及对应的动作。测试用例的生成逻辑很直接——遍历决策表的每一行规则把该行条件取值作为输入把动作作为预期输出。决策表天然的优点是不会漏规则每个条件组合都有一行等着被生成。实际建模时我会把决策表里相同动作的规则做归并用无关项表示条件值不敏感。比如用户等级为普通还是会员只要不参与当次折扣计算就用-占位。这样既能控制表的行数又能保证规则覆盖完整。条件多、规则杂的模块决策表模型比凭感觉写用例可靠得多。2.3 活动图与场景流模型状态机描述状态切换决策表描述规则组合但有些系统的核心在于流程有分支、有并发、有等待、有超时。这种系统用活动图模型更顺手。活动图关注的是动作节点的执行顺序以及节点之间的分支合并条件。比如一个实名认证流程用户提交资料后系统同时进行身份核验和黑名单校验两者都通过才能认证成功任一失败则转入人工审核人工审核超过48小时自动触发短信提醒。这种带并发和超时逻辑的流程用状态机硬画会非常别扭活动图模型却能把分支、合并、等待节点表达得很直观。活动图模型生成用例时重点是覆盖所有分支条件和合并汇合点。每一个判断节点真分支要走到假分支也要走到每一条并行分支的汇合处要验证所有前置分支都完成之后的聚合行为。和状态机模型相比活动图模型更强调主流程与异常分支的完整展开。2.4 语法模型与数据驱动模型还有一类场景适合用语法模型。这类系统没有明显的状态流转但输入的结构非常明确。比如测试一个开放接口它的报文格式、字段长度、必填可选都有规则再比如测试一个配置文件解析器不同配置项的组合决定了解析结果。这种场景可以用语法或形式化描述来定义合法输入集然后通过生成器构造大量合法和非法输入。语法模型在协议测试、模糊测试、解析器测试中特别常见。核心思路是把输入格式定义成产生式规则比如报文报文头报文体校验位报文头版本号长度类型类型取值为请求或响应。生成器在这些规则约束下穷举或随机组合就能得到一批人工很难想到的畸形输入。数据驱动模型则可以理解为参数级建模把一个接口的参数划分成有效等价类、无效等价类、边界值通过笛卡尔积或正交试验生成组合用例。它和决策表模型的区别是决策表关注规则与动作数据驱动模型关注的是参数取值空间。两者的共同点是都适合输入组合致密的场景。2.5 建模粒度的三个把握原则模型形态选好之后最考验经验的是粒度控制。粒度太粗模型生成的用例覆盖不到关键行为价值不大粒度太细模型会膨胀到不可维护。我总结出三个原则。只建模外部可观察行为不建模内部实现。状态一定是用户或外部调用方能感知的比如订单已支付是外部可观察的支付回调正在重试是内部实现细节不该进模型。把内部细节塞进模型除了让模型越来越复杂没有任何测试价值。一个模型聚焦一个业务场景别追求全系统一张图。曾有个团队想把整条核心链路画进同一个状态机结果模型有上百个状态和上千条迁移根本没法评审。后来拆成支付模型、库存模型、物流模型三个独立模型问题迎刃而解。每个模型的规模最好是能让人在十分钟内向新人讲清楚的。模型里要刻意保留抽象层次。具体用户数据、具体金额、具体账号这些不要放进模型模型里只需要有效用户余额不足用户金额超限这样的抽象角色。具体值放到测试数据映射层去解决这一点后面讲落地时还会再展开。3. 从空模型到一批用例一次完整建模与用例生成实操3.1 案例场景登录模块的状态机建模理论讲了不少现在用一个我经常拿来举例的登录模块完整走一遍建模到生成用例的流程。这个模块不算复杂但足够说明核心方法。登录模块的可观察状态有四个未认证、认证失败、已锁定、已认证。事件有输入有效凭据登录、输入无效凭据登录、连续输错触发锁定、锁定到期自动解锁、主动退出登录、使用应用期间会话过期。画出状态迁移后可以得到下面这些规则未认证状态下输入有效凭据进入已认证未认证状态下输入无效凭据进入认证失败认证失败状态下再次输入有效凭据进入已认证认证失败状态下再次输入无效凭据累计达到三次进入已锁定已锁定状态下等锁定期结束进入未认证已认证状态下主动退出登录进入未认证已认证状态下会话过期进入未认证这个模型只有四个状态、七条迁移规则看起来非常简单。但请留意一个细节七条迁移规则里只有两条是登录成功的快乐路径其余五条全是异常和恢复路径。换成手工设计用例大部分测试人员会把重心放在前两条上异常路径往往意思一下就完了。机器遍历则不会偷懒它会认真地从每一条合法迁移路径上生成用例。建模完成后的第一件事不是急着生成用例而是检查模型本身。我会先把状态列出来确认每个状态都至少有一条进入路径和一条离开路径。比如已锁定这个状态进入路径是连续输错离开路径是锁定期结束。如果某个状态只有入口没有出口那它就是一个黑洞用例跑到这里就走不下去了这通常说明建模漏了迁移规则。3.2 覆盖标准怎么选状态覆盖、迁移覆盖还是K路径覆盖模型建好之后生成用例之前要确定一个明确的覆盖标准。覆盖标准决定了机器要遍历到什么程度才停。我在实操中常用四种标准它们之间的差异非常明显。状态覆盖是最基础的标准要求每个状态至少被访问一次。按这个标准上面登录模型最多三五条用例就能达标。状态覆盖能验证每个状态可达但验证不了状态之间迁移是否可靠它适合作为冒烟测试的底线。迁移覆盖是主流推荐标准要求每条迁移至少被走一次。登录模型有七条迁移理论上至少七条用例。迁移覆盖能保证所有合法状态切换都被测到是性价比最高的起步标准。我建议团队从迁移覆盖开始跑因为它对应的用例量在可控范围内而漏测风险已经比手工用例低了一个量级。K路径覆盖是在迁移覆盖之上的强化标准。它要求连续K条迁移的组合都要被执行。K1就是迁移覆盖K2要求每两条相连的迁移组合都被走到。以登录模型为例输错一次后再次输错和输错一次后改成正确凭据登录是两个不同的两两组合分别对应锁定前边界和错误后恢复两条路径K路径覆盖能系统性地把这些组合找出来。代价是用例数随K值上涨比较快一般控制在K2或K3即可。全路径覆盖也就是把所有可能路径都走一遍在有环状态机中几乎都是不可穷尽的。比如未认证—认证失败—未认证这个环可以无限循环全路径覆盖没有终止条件。所以实践中没人用全路径覆盖它只是理论上的理想标准。选好覆盖标准后还要配套定义停止条件。我一般用覆盖率到达目标值配合最大用例条数双条件停止防止生成器在一个复杂模型里陷入死循环。3.3 生成策略里的两个现实问题随机游走与可达性模型和覆盖标准就绪之后生成器开始干活。市面上主流的MBT生成器核心都离不开图遍历算法。一种常见做法是给每条迁移设置权重然后用带权重的随机游走不断走图每走到一条新迁移就把它标记为已覆盖直到覆盖率达标。权重设计很有讲究快乐路径的迁移权重可以给低一些异常路径权重给高一些这样随机游走会有意识地多踩异常场景弥补人类设计用例时的路径偏好。我还想特别聊一下可达性分析。建模之后、生成之前一定要先做一次可达性分析。所谓可达性就是检查每个状态是否能从初始状态经过有限步迁移到达。不可达状态在生成器里表现为永远走不到的孤立节点它会耗费大量无效遍历时间。更关键的是不可达状态往往意味着业务模型有问题。就拿登录模型来说如果建模时错误地画了一条已锁定状态下输入正确凭据直接进入已认证的迁移而真实业务是必须先等待锁定期结束这条迁移就是不可达的因为系统根本不会放行。生成器在遍历前会自动识别到这条不可达迁移等于免费帮我们做了一次模型校验。我几次实际建模中都靠可达性分析发现了需求文档里没写清楚的状态切换逻辑这个附加价值比生成用例本身还大。生成器中一个常用简化逻辑的表达如下这段伪代码能帮你理解背后流程def generate_paths(model, stop_condition): covered_edges set() generated_paths [] while len(covered_edges) target_edge_count and len(generated_paths) max_paths: path [] current model.initial_state() while not stop_condition(path): edge choose_next_edge(current, covered_edges) if edge is None: break path.append(edge) covered_edges.add(edge.id) current edge.target_state generated_paths.append(path) return generated_paths在真实工具中变量名、数据结构会复杂很多但骨架是一致的持续游走持续收集未覆盖迁移直到覆盖标准达标。理解了这个骨架你就能明白为什么MBT能系统性地补上手工用例的路径盲区。3.4 把模型和用例接到测试执行器光有生成的用例路径还不够用例需要被执行才能产生价值。这里最大的工作量在于把模型里的抽象动作映射到具体测试代码。比如模型里有一条未认证状态下输入有效凭据登录进入已认证那么执行层需要把有效凭据映射成一组具体的账号密码把登录映射成一句接口调用或UI操作。这个映射关系我推荐单独维护一份动作映射表不要散落到各个用例脚本里。这样当测试环境账号变化时只需要改映射表不需要改模型和用例。断言同样要做在模型层。MBT一个独特的优势是可以在每个状态进入时检查状态不变式。还是用登录模块举例进入已认证状态时断言当前会话的会话标识是有效的、访问受限接口不再返回登录跳转进入已锁定状态时断言即使使用正确凭据也无法登录。这种在每个状态入口统一检查的断言比手工用例里零散写的断言更系统也不会因为路径不同而漏掉。执行方式上MBT有两种主流选择。第一种是离线生成一次性把所有用例路径生成出来转成可执行的自动化脚本入库管理。第二种是在线执行执行器不预先固定路径而是每走到一个状态动态决定下一步该触发哪个事件边走边测。离线生成的好处是用例可评审、可追踪适合大多数业务系统在线执行的好处是能应对超大状态空间避免离线枚举爆炸适合协议类和参数组合极为复杂的系统。团队起步阶段建议先用离线生成把模型和映射表理顺了再考虑在线执行。4. 落地不是工具问题MBT推行中的常见坑与排查技巧4.1 状态爆炸模型一复杂就失控怎么办这是MBT落地遇到最多的一个问题。模型刚建好的时候状态数量可能只有十几个生成用例非常顺利。随着模块迭代有人觉得这个细节也应该覆盖于是往模型里加状态、加迁移。等你回过神来模型已经膨胀到几十上百个状态生成一批用例要跑很久而且大量用例重复度高、区分度低评审时没人愿意看。解决状态爆炸先要做减法。回到第二章说的粒度三原则检查模型里是不是混进了内部实现状态。例如把缓存刷新中后台任务排队中这种用户感知不到的状态去掉模型规模通常会缩水一半。其次如果状态数确实超过五十个就考虑拆模型把一个全量模型拆成核心主流程模型和异常处理模型分别生成用例再合并去重。拆分模型不仅在技术上更可行在评审时也更容易让人看懂。还有一个更实用的手段是切换覆盖标准。全路径覆盖必然爆炸K路径覆盖K值过大也会爆炸。如果模型规模降不下来就守住迁移覆盖或K2覆盖不要追求过深路径的组合。MBT的价值是系统性覆盖不是路径条数堆砌能把每条迁移走稳已经比手工用例强很多了。4.2 模型漂移需求变了模型没跟上模型漂移指的是系统需求已经变了模型却还停留在旧版本。后果是模型生成的用例和真实行为对不上执行报错大家第一反应是MBT不靠谱。这个坑我之前反复踩过后来总结了一个关键认知模型是一等测试资产必须和被测代码一样做变更管理。具体做法有三点。第一把模型文件加入版本管理和代码使用同样的分支、合并、评审流程。模型不是一次性的设计草图它是会持续演化的可执行资产。第二把模型更新纳入需求变更流程。每次需求变更都要有一个明确的动作项同步更新相关模型。没有这个动作项模型很快就会过期。第三坚持做模型评审。评审时不仅看模型本身还要对着最近一个迭代的需求变更记录逐条确认模型是否已经同步。这三个做法执行到位后模型漂移问题基本能被拦住。实操中我还会做个补充在模型文件头的注释里记录最近变更需求编号和对应代码模块负责人。这样出了问题能快速定位模型是不是过期了而不是在用例层面瞎猜。4.3 生成用例可读性差评审过不了关MBT生成的用例初始形态通常不那么友好。路径表达式、状态编码、动作参数的堆砌看起来就是一堆机器语言。如果直接把这种用例丢到测试管理平台里让团队评审几乎一定会被吐槽看不懂、没法用。这个问题的解法不是放弃MBT而是做人性化渲染。我在实践中会做两层处理。第一层给模型里的每个状态和动作起好读的别名。在模型里用状态名ANONYMOUS渲染时映射成未认证用户状态动作login_success映射成使用有效账号和正确密码登录。第二层在渲染时做路径压缩连续无分支的步骤可以合并路径太长可以分段命名。比如一条路径渲染出来是未认证状态—输入正确凭据—进入已认证—退出登录—回到未认证就比一串编码路径直观多了。还有一个更有效的小技巧让生成工具支持输出两种视图。一种视图是给机器执行的结构路径保留完整状态与事件名另一种是给评审人看的业务用例展示别名和通俗步骤。评审时只看业务视图执行时用结构视图两者通过用例编号关联。这样一来可读性问题就彻底从流程上解决了。4.4 数据准备不足用例能生成执行却失败模型里的数据是抽象的比如有效用户余额不足用户账户锁定用户这些角色映射到测试环境时必须有一批真实存在的测试数据。很多团队试MBT时都栽在这一步用例生成得很顺利跑到执行阶段发现测试环境里没有符合前置条件的账号、没有对应状态的商户、没有可用的优惠券用例大面积失败或跳过。解决思路是把测试数据准备当作MBT落地的一部分来建设而不是等用例生成了才临时补。我在搭建MBT环境时会单独维护一份数据资产清单列出模型里出现的所有抽象角色并为每个角色准备至少一套具体测试数据。有效用户对应正常账号余额不足用户对应一个特定余额的账号账户锁定用户对应一个反复输错密码被锁的账号。这些数据要写进数据准备脚本随测试环境初始化时一起建好。另一个经验是把数据映射和模型动作映射放在同一层管理。动作映射表负责模型动作→API/UI步骤数据资产清单负责抽象角色→具体测试数据两者配合模型本身保持干净数据变化不会反向侵蚀模型。4.5 哪些系统不建议上MBT不是所有系统都值得用MBT。过早或盲目推广会让团队觉得这套方法重而不实用。我总结了几类不太适合的场景。需求极不稳定、产品文档永远滞后于开发的模块不建议第一波上MBT。模型还没建完需求又变了一轮维护成本会比重写手工用例还高。纯视觉校验类场景比如UI布局、样式还原度MBT也帮不上忙因为这类问题本质上是视觉比对不是行为路径问题。一次性探索性测试比如上线前的临时流程验证也没必要为此建模跑几轮手工用例更直接。还有一类是输入参数极其复杂但业务价值很低的模块建模要花大量时间遍历参数组合收益却很小。我常说的一句话是MBT是给要长期维护、有明确状态行为、回归频率高的系统准备的。不符合这三个特征先不要碰。等团队积累了建模经验再把范围慢慢扩大。5. 从一个模块开始MBT小步落地的路线建议5.1 选第一个试点模块的三个标准MBT落地最怕一上来就找整个系统做试点。我建议按三个标准挑第一个模块。状态流转明显、流程分支多、回归频率高这三个条件至少要占两个。比如订单状态流转、审批流、工单处理这类模块天然适合状态机建模阻力最小收益肉眼可见。尽量避免选强计算类模块比如报表引擎、规则引擎这类模块正确性验证的重点在数据和算法不在状态路径建模成本高且价值不突出。另外试点模块的负责人最好对MBT有兴趣愿意花两周时间学习和试错。第一个模块是团队的信心工程选一个大家都能看清价值的模块比选一个关键但复杂的模块重要得多。选定模块后我建议先用手写方式把模型画出来放在白板上拉上开发和产品过一遍。这一步的目的不是生成用例而是统一大家对这个模块状态流转的认知。很多时候模型评审第一轮就能发现开发、测试、产品对某个状态切换的理解不一致这个发现就足够值回建模成本了。5.2 模型评审比用例评审更有价值很多人关心MBT生成的用例怎么评审我的答案是重点评模型不用逐条评用例。因为用例是模型遍历的产物只要模型是对的、覆盖标准是明确的生成用例的可信度就很高相反如果模型本身有错逐条评审用例也发现不了问题大家只是在重复理解同一个错误。模型评审会上我通常带着三个问题过一遍每个状态是否都是外部可观察的合理状态每条迁移是否都是真实业务允许发生的是否存在缺失的状态或迁移第三个问题最关键。我现在已经养成习惯每建一个模型都问一句还有哪些状态变化没有被画出来评审时也请其他成员一起想。之前在一个模拟项目中评审退款流程模型时有同事提出退款处理中如果用户再次发起投诉状态应该怎么变而这条路径在需求文档里确实存在却被大家集体忽略了。后来它成了那个模块最高价值的用例。所以如果团队还在纠结MBT生成的用例要不要一条条评审我的建议是调整思路把评审资源从用例层挪到模型层。模型对了用例层的风险就小了大半。5.3 用覆盖率数据说服团队MBT在小范围跑通后要让它继续推广光说模型好是不够的要用数据说话。我会在试点模块上做一个对照统计同一个模块手工用例集覆盖了多少条迁移路径MBT生成的用例集覆盖了多少条迁移路径。差距通常会非常直观。比如一个手工用例评审过三轮的登录模块实际覆盖到的状态迁移可能只有六成而MBT在迁移覆盖标准下可以做到百分百。更有说服力的是缺陷数据把MBT生成、而手工用例漏掉的那几条路径对应的测试结果单独列出来标注其中发现了哪些历史缺陷。当我第一次在一个试点模块上跑出三个手工用例集从未覆盖过的缺陷时团队对MBT的态度立刻从怀疑变成了主动问下一个模块什么时候建模。原因是人只相信自己看到的结果。不过我要提醒一句不要拿着MBT的覆盖率数据去批评团队之前的工作。MBT的价值不是证明谁做得不好而是提供一种更系统的补漏方式。用数据展示增量收益而不是翻旧账团队才愿意配合。说的直白一点MBT不是给测试人员增加负担的技术它本质上是把测试人员从人肉枚举器的角色里解放出来。人真正擅长的是判断业务价值、理解需求边界、定义什么该测这些判断最后沉淀进模型里至于那些繁琐的排列组合、路径枚举交给图遍历算法去跑既快又全。如果你准备试MBT我的建议是从一个你最熟悉的小模块开始用两周时间把模型建出来跑一批用例然后拿给团队里最有经验的人看一眼。他大概率会说一句哦这条路我们以前从来没测过。那一刻你就明白了这套方法到底值不值得继续投入。