1. 为什么我会推荐把MBT放进测试技术栈1.1 传统用例设计的三道坎做测试这行时间长了你会慢慢发现一个有意思的现象真正让人头疼的不是测试执行而是测试设计。需求评审完了开发方案定了接下来就是用例设计。但用例写得好不好很大程度上取决于个人经验。有人能想到异常的排列组合有人只能覆盖到主流程。这份依赖个人经验的“手艺活”在项目交付压力上来之后很容易变成走过场补几个主流程、加几个异常分支就算交差。至于系统里那些交错的状态流转、边界条件、组合路径漏掉是常态不漏才是运气。第二个坎是覆盖度没法度量。执行完之后团队能拿出来的数字往往是“用例总数、通过率、缺陷数”但很少有人能回答一个问题核心业务状态流转我们的用例到底覆盖了百分之多少代码覆盖率可以靠工具测出来但业务行为覆盖度传统用例设计里几乎没有抓手。第三个坎是需求变更的连带成本。系统状态一调整接口一改线下设计好的用例就要人工逐条改。改的时候靠人肉判断哪些用例受影响很容易顾此失彼。我见过不少团队需求从一版翻到二版时用例库里躺着一堆已经失效的步骤没人清理也没人发现。这三道坎叠在一起结果就是测试设计变成了“既费人、又难量化、还怕变更”的环节。这也是我后来认真去研究和落地基于模型的测试MBTModel-Based Testing的根本原因。1.2 MBT到底做了什么改变MBT的核心思路是把“写用例”这件事拆成两步先建模型再生成用例。具体来说建模阶段把被测系统的关键行为用一种结构化的形式表达出来比如状态机、流程图、决策表。模型里清晰地写清楚系统有哪些状态、哪些动作能触发状态变化、哪些条件下流转成立、哪些数据组合会产生什么结果。建完模型之后测试用例的生成就从“人脑推演”变成了“算法遍历”由工具基于模型自动跑出满足指定覆盖准则的路径和条件组合。这一步转变带来的好处是结构性的。第一覆盖度可以量化和追溯。模型里的每个状态、每条边都对应一个业务行为节点生成的用例覆盖了百分之多少的状态、多少条迁移路径一清二楚。这个数字直接回答了我刚才说的那个痛点核心业务行为到底测够了没有。第二测试设计和实现解耦。模型变了重新生成一套用例就行。底层负责执行的部分单独维护不受用例变更影响。需求的修改落到模型文件上比跑到用例库里逐条改要快得多也不容易漏。第三用例设计从“凭经验”变成“靠结构”。模型把需求里的显性规则和隐性分支都摆到台面上评审模型本身就是评审需求。很多需求里的漏洞在建模阶段就会暴露出来根本等不到运行阶段。我用一个生活化的类比来解释传统测试设计像是工人拿着一张手绘清单去仓库拣货清单漏了哪项货就漏拣了MBT则是先把仓库货物分布画成一张地图再让机器按路线自动生成拣货清单。地图只要画得准清单的完备性就有了保障。1.3 谁最适合先试水MBT不是银弹不是所有系统都适合一上来就建模生成用例。从我落地项目的经验看最适合先试水的对象有几个特征。第一业务状态和流程复杂度高于普通CRUD。登录态、订单状态流转、审批流程、设备状态切换这类“翻来覆去地变状态”的业务用模型表达收益最大。如果系统绝大部分接口都是无状态的增删改查那传统用例设计就够了上MBT反而多余。第二团队有稳定且可回归的测试目标。没有自动化执行能力MBT生成的用例只能停留在文档层面。反过来想MBT本来就是为自动化回归设计的手工执行生成的批量用例体验不会好。第三模型维护的投入能换来真实回报。说白了系统迭代频繁流程复用度高模型的价值就大。如果系统半年才发一次版改动又小模型就算建出来也是“躺着睡大觉”。我比较推荐的切入路径是先挑一个状态复杂、缺陷风险高的小模块试水把模型建起来、工具链跑通再用结果去说服团队扩大范围。一上来就铺全量建模工作量会把团队吓退。2. 建模选型模型不是画漂亮的图而是可执行的测试规格2.1 先搞清模型的三个基本组成MBT里的模型听起来抽象落到实践上其实就三类要素状态、动作、守卫条件。状态描述的是系统在某个时刻的稳定情形。比如“订单待支付”“订单已支付”“订单已取消”这些都属于状态。状态是模型里的节点。动作是导致状态发生迁移的事件。用户点击“立即支付”、定时任务触发“超时关单”这些动作对应模型里的边也就是从当前状态到下一个状态的迁移弧。守卫条件决定了动作在当前状态下是否真的能生效。比如“仅当订单归属当前用户且订单状态为待支付时取消操作才被允许”。“当前用户是订单归属人”和“订单状态为待支付”就是两条守卫条件。条件不满足动作即便发生了也不能触发迁移。把这三类要素放到一张图里就是一个非常标准的状态机模型。我见过不少刚入门的人建模时喜欢把页面上所有的按钮都摆进去结果把模型画成一张密密麻麻的蜘蛛网。问题的根源在于——弄混了“动作”和“操作入口”。按钮是操作入口但真正推动业务往前走的是动作背后的业务语义。比如“提交订单”和“提交失败重试”在模型里可能是同一个动作的两种守卫分支不该劈成两个节点来表达。建模前先问自己一个问题这个动作发生之后系统的业务状态到底变没变没变的别往模型里塞。这个原则能帮你砍掉至少三成无效内容。2.2 常用模型视图怎么选不同业务形态适合的表达模型也不同。我列一个简表方便对照着选。模型类型表达重点适合的场景产出用例类型有限状态机FSM状态、事件、迁移、守卫订单、审批、协议状态机等强状态业务状态覆盖、迁移覆盖、路径覆盖活动图/流程图业务流程、分支、合并、并行端到端业务流程、跨系统交互流程流程路径覆盖、条件组合决策表/因果图条件组合与结果之间的映射规则引擎、促销计算、费用计算等强逻辑业务条件组合覆盖、MC/DC变体时序图模型消息交互顺序、超时、重发消息中间件、通信协议、分布式交互消息序列覆盖、超时场景选型的时候不要贪多。一个模块用一种主模型表达清楚比混用两三种模型更可控。我的习惯是先看业务是否有“明显的状态集合”如果有直接用状态机如果状态感不强、但流程分支复杂就用活动图如果业务本质是“一堆条件算出一堆结果”比如优惠计算、费率计算决策表是效率最高的。这里要特别提醒一个点模型最终要能被工具解析不是画给人看就完了。所以建模动作一定发生在工具链支持的范围里。建模之前先看工具支持哪些模型别在画图工具里画了一堆漂亮的图最后没法导入生成器。2.3 抽象级别的选择是建模成败的分水岭模型该建到多细是MBT落地中最容易失衡的地方。建模过细细到页面上每个输入框的每种校验模型会迅速膨胀状态爆炸、边数激增生成出来的用例也长得没法看。建模过粗又表达不出关键业务逻辑生成的用例全部点到为止覆盖不到真正的风险点。判断抽象级别是否合适的标准我用了很久确实靠谱模型里的一个迁移能否对应到测试实现里的一个“验证步骤”。如果能那这个抽象层次是合适的。比如“用户发起退款申请”对应到系统里就是调一个退款接口、传入订单号并携带会话凭证验证返回字段“退款单号”不为空。这就是一个可映射的迁移。如果“用户发起退款申请”对应到界面上要操作五个控件、等三个异步回调那就说明抽象层级偏低了要把这些动作封装成一个“用户动作”来建模不然适配层会写得非常痛苦。另外一个容易被忽略的点是模型是给测试用的不是给需求文档补充说明的。不需要把所有非测试关注点的内容都塞进来。跟这次测试目标无关的状态细节可以大胆砍掉。测试关注谁的迁移模型就保留谁。3. 实操核心用开源工具把模型变成用例3.1 定义状态机与守卫条件的落地写法理论讲了这么多真正动手建模型的时候写法是关键。这里以开源工具GraphWalker为例它在MBT工具链里算是比较容易上手的一款模型用JSON或GraphML描述输入模型再加上覆盖准则就能输出可执行的测试路径。先看一个极简的登录状态机模型感受一下JSON的书写方式。{ name: loginModel, generator: random(edge_coverage(100)), model: { states: [ {name: LoggedOut}, {name: LoggingIn}, {name: LoggedIn} ], edges: [ {name: submitCredentials, sourceState: LoggedOut, targetState: LoggingIn, weight: 1}, {name: loginAccepted, sourceState: LoggingIn, targetState: LoggedIn, weight: 1}, {name: loginRejected, sourceState: LoggingIn, targetState: LoggedOut, weight: 1} ] } }上面的JSON描述了三个状态未登录、登录中、已登录。三条边分别对应“提交凭证”“登录成功”“登录失败”。generator那一行random(edge_coverage(100))的意思是用随机策略生成路径目标是覆盖全部100%的迁移边。很多人第一次看到这个JSON会问为什么没有写守卫条件GraphWalker这类工具的通用做法是把守卫条件的判断逻辑放在测试实现里也就是适配层。模型层面通常只描述结构化的“候选路径”而“这条路径能不能真正走通”由适配层的代码做运行时判定。这个设计初看觉得麻烦细想其实是合理的模型保持足够的抽象度就不容易被底层实现细节绑架。如果系统从接口调用改成消息触发只要业务状态没变模型还是那份模型改的只是适配层。3.2 覆盖准则的选择与生成策略模型可以生成无限多条路径但没人会跑无限多。所以MBT里必须有一个“收口”的动作这个动作就是选择覆盖准则。最常见的几个准则按性价比排序我先从低到高说。状态覆盖要求生成的路径把这些状态都走到至少一次。这是最弱的准则一般只用来做冒烟测试。迁移覆盖要求每条迁移边都至少被走过一次这是MBT落地的底线和默认选项对应到业务上就是“每个业务动作的每个流向分支都被验证过”。多条件覆盖要求守卫条件里的每个原子条件分别取真和假适合规则敏感型业务。路径覆盖则穷举所有可达路径的组合代价最高一般只在核心模块的少量关键路径上应用。我在实际项目里极少直接用全路径覆盖那会带来严重的组合爆炸。通用的打法是迁移覆盖打底再用条件覆盖补关键业务规则最后手工补充几条模型没有覆盖到的端到端异常场景。用GraphWalker的话generator的写法可以这样配# 迁移覆盖 generator quick_random(edge_coverage(100)) # 状态覆盖适合快速冒烟 generator random(vertex_coverage(100)) # 条件覆盖适合规则型业务注意模型需支持条件表达 generator random(condition_coverage(100))关于生成算法我个人的观察是随机策略在大多数业务场景下反而是最优解。原因在于确定性深度优先算法生成的路径序列往往比较固定容易偏向某几个分支组合造成覆盖盲区随机策略配合覆盖向导能在同样的路径长度下探索到更多组合。3.3 适配层设计决定自动化成败MBT的模型生成出来的本质上是“带逻辑步骤的路径描述”。要让这些路径真正跑起来必须有一个适配层把模型里的每个动作翻译成真实的系统操作。我习惯把适配层设计成一张“动作映射表”。模型里的动作名是抽象名适配层里提供同名的方法方法内部去调用真实接口或操作UI。以登录为例模型里的“submitCredentials”动作在适配层的实现可能长这样class LoginAdapter: def submit_credentials(self, account, token): resp auth_api.login(account, token) return resp.status_code 200 # 返回布尔值供守卫条件判断这里有一个很有争议但实际有效的实践把适配层的“断言”尽量后置。也就是说适配层方法只负责执行动作和返回结果特征真正的业务断言放到路径执行完之后统一做。原因在于MBT生成的是一个连续路径你在中间某个迁移处卡了断言可能因为环境时序问题产生误报而把断言集中到路径终点反而能更准确地评估整条业务链路的正确性。适配层要不要用UI自动化我的建议是优先接口层。原因太现实了——UI自动化本身就带一大堆稳定性问题再叠加MBT的随机路径你会分不清是产品出问题了还是元素没找到还是路径设计有问题。接口层的适配层要稳定得多适合先把整个MBT链路打通。打通之后如果有端到端验证的硬需求再在少量关键路径上接UI操作。4. 完整小案例登录态下购物车下单流程的MBT落地4.1 业务与模型定义空讲概念没有说服力我用一个带登录态的购物车下单流程把整条MBT链路完整走一遍。这个系统的核心业务逻辑是这样的用户下单前需要先登录未登录时只能浏览和加购不能提交订单购物车有商品才能进入结算结算时校验商品库存和用户地址任一不满足就不能提交成功订单提交成功后就进入“待支付”状态超时未支付自动取消。整理一下关键状态未登录LoggedOut购物车有商品CartNonEmpty结算中Checkout订单已提交OrderSubmitted订单已支付OrderPaid订单已取消OrderCancelled关键动作和守卫条件加购动作仅当未登录或已登录均可加购后进入购物车有商品状态提交订单仅当已登录且购物车有商品且有有效地址进入结算中结算成功仅当库存和地址校验通过进入订单已提交支付成功仅当订单归属当前用户进入订单已支付超时未支付进入订单已取消。注意一个容易出错的地方模型里的守卫条件我不建议直接写判断逻辑而是通过生成器和适配层配合实现。GraphWalker这类工具在运行时由适配层执行“这个动作能否发生”的判断所以建模阶段只需要把“可能发生的迁移”表达出来即可。4.2 模型文件与生成结果对应的GraphWalker简化模型我写成下面这样{ name: orderFlowModel, generator: quick_random(edge_coverage(100)), model: { states: [ {name: LoggedOut}, {name: CartNonEmpty}, {name: Checkout}, {name: OrderSubmitted}, {name: OrderPaid}, {name: OrderCancelled} ], edges: [ {name: browseAndAddCart, sourceState: LoggedOut, targetState: CartNonEmpty, weight: 1}, {name: login, sourceState: CartNonEmpty, targetState: CartNonEmpty, weight: 1}, {name: goCheckout, sourceState: CartNonEmpty, targetState: Checkout, weight: 1}, {name: submitOrderSuccess, sourceState: Checkout, targetState: OrderSubmitted, weight: 1}, {name: payOrder, sourceState: OrderSubmitted, targetState: OrderPaid, weight: 1}, {name: orderTimeoutCancel, sourceState: OrderSubmitted, targetState: OrderCancelled, weight: 1} ] } }这个模型没有画出登录失败的迁移是刻意为之——这次测试目标的重点落在下单支付主链路登录失败分支由另一套专门的登录状态机覆盖。模型设计时做适当的剪裁可以让跨界面的测试焦点更集中这也是前面提到的抽象级别选择在实操中的体现。生成结果通常是这样一组路径browseAndAddCart - login - goCheckout - submitOrderSuccess - payOrder browseAndAddCart - goCheckout - submitOrderSuccess - orderTimeoutCancel第二条路径会产生一个“有趣”的动作序列未登录状态下直接进入结算。适配层的守卫条件会在这里拦截下来未登录用户不允许提交订单于是这条路径的执行结果是“订单提交失败”而这条失败路径被模型自动发现正是我们想要的覆盖价值。如果人工设计用例这类“未登录直接进结算”的场景大概率会被列进去但仍需手工逐条写有了模型它是自动从状态拓扑里冒出来的。4.3 执行与覆盖率分析生成路径之后我在适配层里把动作一一实现。每个方法内部调用真实的下单接口、支付接口或在本地的模拟环境里跑。执行完成后我用工具导出一份执行报告看起来大概是这样的覆盖项覆盖率说明状态覆盖100%6个状态全部走到迁移覆盖83%6条边走了5条登录失败分支未覆盖路径组合2条执行8条唯一生成器随机选了2条可执行路径83%的迁移覆盖并不意外。一方面“login失败”这条边被单独拆到其他模型里另一方面随机策略在有限步数内本来就不会穷尽所有路径。这时候就需要做一件事把未覆盖的边手工补测还是调整生成策略重新跑。我的经验是第一步先看未覆盖的边是不是“值得测的边”。如果那条边代表的业务风险很低比如日志级别的分支直接标记为豁免即可不用为了凑100%而不计代价。如果未覆盖的边是高风险的比如“支付回调失败”那就调整模型权重或单独为这条边设计一条起点到终点的路径。最怕的是抱着“覆盖率达到100%才算测完”的心态把MBT当成覆盖率的数字游戏那会做出很多没有实际价值的测试。执行过程中我还会做一次静态核对拿模型里的状态清单和需求文档里的状态枚举逐条比对确认没有遗漏或命名不一致。这一步看起来多余但对发现需求和实现不一致的问题非常高效。很多测试周期的需求漏洞就是在这个核对环节被揪出来的。5. 常见问题与排查技巧实录5.1 高频问题排查速查表用MBT时遇到的问题来来回回就那么几类我整理成一张速查表方便遇到问题时对着排查。问题现象根本原因解决思路模型能建但生成用例数量巨大根本跑不完覆盖准则过强或模型状态过细改用边覆盖把低风险状态从模型中剪掉增加动作权重约束生成的路径中有大量“重复且无聊”的循环模型存在死循环路径且守卫条件未限制在生成器中限制最大步数或给循环边降低权重模型总是生成不可达的路径边的守卫条件只在适配层判断模型本身无法预判把部分高确定性守卫条件提升为模型内的“禁止转移”配置用例执行频繁失败且失败位置随机适配层断言过于前置或执行环境时序不稳定把断言后置到路径终点给异步操作增加显式等待模型维护跟不上需求变更建模过程与需求评审脱节把模型评审纳入需求评审流程需求变更带动模型变更团队反馈模型看不懂、不敢改模型内容与代码结构耦合过深模型抽象回归业务层面隐藏内部实现细节除了这张表我再提示一个被很多人忽视的点模型文件的版本管理。模型和普通代码一样会随着需求演进不断修改。务必把模型文件和生成结果快照一起纳入版本控制。这样当某个测试周期出现奇怪的用例行为时你可以回溯“这个用例是模型从哪个版本生成出来的”排查效率能提升一个量级。5.2 三条我踩过的坑第一建模时把接口权限关系和业务状态混在一起表达。比如把“管理员才可取消”和“订单已提交才可取消”写在同一层造成守卫条件膨胀。后来我的处理方式是权限类守卫下沉到适配层由环境准备阶段保证当前用户具备对应权限模型里只保留业务状态类守卫。这样一来模型干净很多生成的路径也不会被权限分支干扰。第二初始阶段过度追求“模型自动生成一切”。有些团队成员觉得上了MBT用例、数据、预期结果都要自动产出。实际上MBT最擅长的是“结构覆盖率高的路径生成”数据准备和预期结果校验仍然需要测试工程师来定义。把预期不切实际地放到“全自动”的目标上前期收益会被摊薄团队容易丧失信心。合理预期是让模型承担最枯燥、最容易遗漏的路径推演人的精力集中到结果验证和边界补充上。第三忽略对生成路径的可读性处理。工具生成的路径名如果直接采用模型里边的名字执行报告会变成一串很难懂的英文编码评审时没人愿意看。我后来养成了一个习惯在适配层里给每个动作加上一个中文业务描述。执行报告输出时同时带技术名和业务名。这一改动很小但对跨团队协作的价值非常大项目负责人能直接看懂测试覆盖的业务流而不用费力做二次翻译。这三条坑都是文档里不会写、只有跑完一个完整迭代才会体会到的。分享出来是想让后人少走一程弯路。MBT这套东西能不能在一个团队里活下来很多时候不是工具的问题而是建模习惯和执行预期有没有摆正。把模型当成活的测试规格来养它就会在每一个需求迭代里持续产生价值如果当一次性交付物来建那它很快会成为没人维护的负担。就我个人的体会而言MBT最迷人的地方不是生成用例节省了多少时间而是它逼着你把业务行为想清楚后再动手测这件事本身就是对测试质量最好的投资。
阅读完成 · 觉得有帮助?