首页 / 资讯中心 / 文章详情

测试代码一键生成业务流程图:AST解析与PlantUML渲染实战

测试代码一键生成业务流程图:AST解析与PlantUML渲染实战 ★ FEATURED ARTICLE
做测试这行画流程图是绕不开的活。需求评审要画、用例设计要画、缺陷分析还要画。刚开始我用界面拖拽画图一张复杂点的业务链路图往往要折腾半小时以上而且代码一改图又得重新画。后来我把自己这套“代码一键生成业务流程图”的Skill沉淀下来输入测试代码脚本自动抽出步骤和分支直接渲染成流程图整个过程十秒以内。这篇文章就把这套方法完整拆开从方案选型到实现细节都讲一遍适合所有被画图逼疯的测试工程师也适合想给团队提效的测试开发同学。1. 为什么测试工程师需要“代码生成流程图”1.1 手工画图的三个常见痛点先聊痛点。测试工程师画流程图的场景其实比开发还多编写测试用例要先理清业务链路跨系统联调要跟其他团队对齐数据流评审会上要讲清楚场景覆盖出了线上问题还得快速梳理受影响的功能路径。手工画图最大的问题不是“不会画”而是“画了就没法改”。比如我接手过某订单系统的测试需求文档里的流程图是第一版画的等代码迭代第三版之后那张图跟实际业务已经完全对不上了。我尝试抽时间更新发现改动牵一发动全身一个分支条件的变更后面十几个节点都要挪位置画图工具里对齐、拉线、调整箭头半小时下不来。第二个痛点是风格不统一。同一个系统A同学画出来的图用矩形画步骤B同学用圆角矩形C同学直接套用别的项目的模板。放到评审会上大家要先花五分钟搞清楚“这个颜色代表什么”“虚线箭头又是什么意思”。流程图的阅读成本比它本身的内容还要高。第三个痛点更隐蔽流程图和测试代码是两份独立资产维护它们需要双倍精力。很多团队实际上已经放弃了同步最终流程图变成了文档库里的“僵尸资产”。新同事入职翻到那张图对照代码一看发现一半路都对不上反而起了误导作用。我后来反思这事不能靠意志力解决得靠工具解决。1.2 代码生成带来的四个直接收益代码生成业务流程图核心思路是把“画图”变成“生成图”。流程图的唯一事实来源就是测试代码本身脚本负责解析代码结构、提取步骤和分支再自动渲染出图。这样带来的第一个收益是“永远同步”。代码改了跑一次脚本图就跟着变了。测试用例本身写得清晰生成的图就清晰用例写得烂生成的图也烂——等于给代码质量加了一个视觉化放大器。第二个收益是“快”。手工画图半小时起步脚本生成只要几秒。而且不只是生成一次是每一次修改都能快速重新生成。这个速度差异改变了工作习惯以前舍不得改流程图因为改一次太痛现在随便改反正是自动的。第三个收益是强制结构化。为了让脚本能解析测试代码里的步骤命名、分支条件、断言位置都要写得规整。这反而逼着我养成好习惯每个测试函数都有清晰的docstring每个关键动作都有明确的注释每个分支条件都能被机器识别。久而久之测试代码的可读性也上来了。第四个收益是带团队。主流程、异常分支、关键断言一张图就能全看到。给新同事讲业务打开图比对着几十个测试文件讲效率高太多。2. 方案选型从流程描述到图形渲染2.1 主流渲染方案怎么选代码生成流程图第一件事是选渲染方案。我对比过市面主流的四类做法这里直接给参考意见。第一种是Graphviz它用点node和边edge描述图结构渲染引擎自动布局。优点是灵活能控制细节缺点是要自己写布局脚本团队里大多数人上手比较慢。第二种是PlantUML用文本描述流程图、时序图、泳道图。好处是语法接近自然语言start、if、else、endif这种关键字一看就懂而且对中文支持不错。它内部其实也用Graphviz做布局引擎但对使用者隐藏了复杂度。第三种是Mermaid也是文本描述但生态偏向Markdown文档配合一些在线文档平台使用很舒服。不过它对复杂流程图的支持没有PlantUML那么稳条件分支嵌套多的时候排版容易失控。第四种是自己写图形渲染用画布库直接输出图片。适合产品形态特别定制化的场景但对测试团队来说工作量太大一般不考虑。我个人最终选了PlantUML。原因很简单团队里的测试工程师不是前端大家需要的是“十分钟能上手”的文本语法而不是一套图形开发框架。PlantUML的语法天然贴合流程控制结构if/else/endif的写法跟代码里的分支判断在认知上是一致的解析器写起来也省事。2.2 整条主链路怎么设计方案定了PlantUML接下来就是设计主链路。我的整套流程分为三段测试代码文件 - AST解析 - 中间流程模型 - 文本渲染 - PlantUML图片第一步是读取测试代码用Python的AST模块去解析而不是用正则硬匹配。AST能拿到函数的真实结构哪些是调用、哪些是分支、哪些是断言。用正则在简单场景能跑通一旦遇到嵌套分支、多行表达式、装饰器这些复杂语法就废了。第二步是把AST解析出的结构整理成“流程模型”。这个模型不关心渲染细节只描述流程的节点、分支、连接关系。我单独建了一个中间层这样未来想换渲染器比如从PlantUML换成Graphviz只需要改最后一层前面的解析逻辑完全不用动。第三步是渲染。把流程模型映射成PlantUML文本再调用PlantUML引擎生成PNG或SVG图片。这一步核心的工作是字符串拼接和特殊字符转义坑主要在中文引号、括号、换行这些字符上后面会详细讲。2.3 中间建模为什么不能跳过有些朋友会问解析完AST直接拼PlantUML文本不就行了为什么还要加一个中间模型我最初也这么干过写了两版之后发现不行。直接拼接的问题在于测试代码的结构和图形表达并不是一一对应的。比如代码里的一个if分支PlantUML里对应if/else/endif但如果你遵循的是BPMN风格它对应的是一个判断框加两条分支线表达方式完全不同。如果解析逻辑和渲染逻辑耦合在一起所有改渲染风格的请求都会拖垮解析代码。中间模型就是一层保护壳。我定义了三类基础结构StepNode表示一个动作节点BranchNode表示分支判断AssertNode表示校验断言。解析器只负责把代码装进这三类结构里渲染器只负责把这三类结构画出来。职责清了代码好维护测试也好写。其实这里有个生活化的类比中间模型相当于“菜谱”和“做菜”之间的食材清单。菜谱告诉你做什么菜食材清单确定要买什么、买多少。你从菜谱整理出清单再从清单去采购每一步都清楚明白不至于买错或漏买。3. 核心机制从测试代码到流程图的映射规则3.1 解析测试代码的几种常用做法要让代码“一键生成”流程图首先得解决“从哪拿信息”的问题。我总结下来常见的做法有三条路子。一条是基于pytest的hook机制在测试执行时收集用例元数据。好处是拿到的信息非常准确用例名、参数化数据都是现成的缺点是必须在测试运行时介入对于只想离线梳理流程的场景反而重了。一条是静态正则扫描用模式匹配关键动作。比如匹配def test_开头的函数匹配assert断言语句匹配注释里的步骤描述。上手最快但只适用于代码写得非常规整的小项目一旦有换行嵌套就抓瞎。还有一条就是我最终采用的AST解析。Python代码在运行之前会被编译成抽象语法树AST里包含了代码的全部结构信息。我可以看到函数体里的每一条语句、if条件的表达式、函数调用的具体参数。关键是它不需要执行被测代码完全静态分析安全又干净。AST解析还有一个额外优势它不关心你用的是pytest还是unittest只要是Python代码都能解析。对于既有pytest风格用例又有unittest风格用例的老项目这一点的价值特别大。3.2 节点、分支、断言怎么映射我把测试代码的结构归纳成三类跟中间模型的三种节点一一对应。第一类是功能步骤。看代码里的函数调用比如login(...)、submit_order(...)、query_order_status(...)这类语句表示系统执行的一个动作映射为流程图的行动节点。第二类是分支判断。看代码里的if/elif/else。分支条件决定了测试走哪条路映射为流程图的判断节点。比如“库存是否足够”“金额是否超过限额”这类分支正是测试场景的差异化来源必须画清楚。第三类是断言校验。看代码里的assert语句和pytest.raises上下文。断言是测试的核心产出映射为流程图的校验节点我会把断言的期望值写进节点文本里例如“断言订单状态为已创建”。这样阅读图的人一看到校验节点就知道测试到底在验什么。除这三类之外还有两个需要处理的特殊情况with语句和for循环。with配合pytest.raises时表示异常场景我把它映射成一个“预期异常”节点for循环会映射成一个带返回箭头的“循环”节点后面再展开讲。3.3 第一个可运行的核心脚本光说不练不行下面给一个能跑的最小核心脚本。它读取一份Python测试文件提取其中的步骤调用和断言输出PlantUML文本。import ast class FlowExtractor: 从Python测试代码中提取流程结构 def __init__(self): self.nodes [] # 节点列表 self.edges [] # 连接关系 self.cursor None # 当前所在节点 def parse(self, source_code: str): tree ast.parse(source_code) for node in tree.body: if isinstance(node, ast.FunctionDef): self._parse_function(node) def _parse_function(self, func_node): func_name func_node.name # 用函数的docstring作为流程起点说明 docstring ast.get_docstring(func_node) or func_name self._add_node(action, docstring) for stmt in func_node.body: if isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef)): continue self._parse_stmt(stmt) def _parse_stmt(self, stmt): if isinstance(stmt, ast.If): condition ast.unparse(stmt.test) self._add_node(branch, condition) for child in stmt.body: self._parse_stmt(child) if stmt.orelse: for child in stmt.orelse: self._parse_stmt(child) elif isinstance(stmt, (ast.Call, ast.Expr)): expr stmt.value if isinstance(stmt, ast.Expr) else stmt if isinstance(expr, ast.Call): if isinstance(expr.func, ast.Name): self._add_node(action, expr.func.id) elif isinstance(stmt, ast.Assert): self._add_node(assert, ast.unparse(stmt.test)) elif isinstance(stmt, ast.With): context_text ast.unparse(stmt) self._add_node(action, fwith: {context_text[:40]}) for child in stmt.body: self._parse_stmt(child) def _add_node(self, node_type, label): node_id fnode{len(self.nodes)} self.nodes.append({id: node_id, type: node_type, label: label}) if len(self.nodes) 1: self.edges.append((self.nodes[-2][id], node_id)) if __name__ __main__: sample_code def test_create_order_success(): 用户下单成功流程 login(customer) open_product_page(1001) add_to_cart(1001, num2) submit_order(address_id123) assert response.status_code 200 assert response.json[order_status] CREATED extractor FlowExtractor() extractor.parse(sample_code) for node in extractor.nodes: print(node[type], |, node[label])这段代码的逻辑很简单遍历AST的顶层函数定义再依次处理函数体内的语句。ast.unparse是Python 3.9以后提供的方法如果用的是旧版本建议升级或者自己写表达式拼接。运行这段代码输出应该类似action | 用户下单成功流程 action | login action | open_product_page action | add_to_cart action | submit_order assert | response.status_code 200 assert | response.json[order_status] CREATED这已经是一个流程的骨架了。接下来需要把这份结构映射成PlantUML文本。4. 从零实现一个flow-skill脚本4.1 环境准备和目录结构在实现完整版之前先把环境准备好。我推荐用Python 3.10及以上版本原因主要是ast.unparse在3.9才可用但3.9的某些边缘语法处理得不够好3.10更稳。需要安装的Python包只有两个一个是plantuml它是PlantUML的Python封装负责调用渲染引擎另一个是click我用它做命令行参数解析。有洁癖的朋友也可以直接用标准库的argparse但是click的代码写起来舒服很多团队里上手也快。目录结构我是这样组织的flow-skill/ ├── cli.py # 命令行入口 ├── extractor.py # AST解析器 ├── model.py # 流程模型定义 ├── renderer.py # PlantUML文本生成 ├── config.yaml # 可选的样式配置 ├── output/ # 生成的流程图输出目录 └── tests/ └── test_order.py # 被测的测试代码样例这个结构也不算标准答案但有一个原则值得坚持解析、模型、渲染三层分开。尤其是renderer.py和extractor.py如果你把文本拼接逻辑混进AST遍历里后面想加一种节点类型就会特别痛苦。4.2 渲染层的具体实现接下来是渲染层。用中间模型里的节点和边拼出一份合法的PlantUML文档。# renderer.py PLANTUML_TEMPLATE startuml {skin_config} left to right direction {content} enduml SKIN_CONFIG skinparam defaultFontName Microsoft YaHei skinparam nodeFontSize 12 skinparam shadowing false def render(nodes, edges): 把中间模型渲染成PlantUML文本 lines [] node_ids {} for node in nodes: node_id node[id] label node[label] escaped escape_label(label) if node[type] action: lines.append(f:{escaped};) elif node[type] assert: lines.append(f:[{escaped}];) elif node[type] branch: lines.append(fif ({escaped}) then (是)) else: lines.append(f:{escaped};) # 连接线 for src, dst in edges: lines.append(f{src} -- {dst}) return PLANTUML_TEMPLATE.format( skin_configSKIN_CONFIG, content\n.join(lines) ) def escape_label(text: str) - str: 处理PlantUML中的特殊字符 text text.replace(\\, \\\\) text text.replace(\, \\\) text text.replace(\n, ) text text.replace(|, /) return text这里面有两个细节要留意。第一是left to right direction。这是我在折腾了无数张“竖长图”之后加上的。默认的竖向布局在流程图分支多的时候会变得很长打印出来要分好几页改成横向布局之后整体宽度可控分支的层次感也更直观。有的团队风格偏竖向这个参数可以放到配置里按需切换。第二是断言节点我用方括号[内容]包起来这样在图里能明显区分“动作”和“校验”一眼过去就知道测试的验证点在哪。PlantUML支持节点形状的自定义这类小调整对阅读体验提升很大。4.3 命令行入口与一键封装有了解析和渲染还差一个让组合跑起来的入口。我封装了一个cli.py用法如下python cli.py tests/test_order.py -o output/order_flow.png核心代码如下# cli.py import click from pathlib import Path from extractor import FlowExtractor from renderer import render click.command() click.argument(source_file, typeclick.Path(existsTrue)) click.option(--output, -o, defaultoutput/flow.png, help输出图片路径) click.option(--format, -f, defaultpng, typeclick.Choice([png, svg])) def main(source_file, output, format): 读取测试代码并生成业务流程图 source_path Path(source_file) source_code source_path.read_text(encodingutf-8) extractor FlowExtractor() extractor.parse(source_code) plantuml_text render(extractor.nodes, extractor.edges) plantuml_code.save(plantuml_text, output, formatformat) click.echo(f流程节点数: {len(extractor.nodes)}) click.echo(f输出文件: {output}) if __name__ __main__: main()plantuml包内部会调用本地的PlantUML渲染器第一次运行可能会比较慢因为要初始化Java环境。如果你不想依赖Java也可以把生成的.puml文件直接拖到在线渲染器里出图不过团队内部使用还是建议本地搭好环境稳定也不限文件大小。这里有一个工程细节用click的好处是天然支持--help团队成员拿到脚本不需要看文档敲一句python cli.py --help就能知道怎么用。脚本的入口写得越傻瓜别人越愿意用。4.4 支持更多场景的扩展思路基础版本跑通之后再往深处走。我根据真实项目需求给脚本加了几个扩展点。第一个是参数化用例。pytest的pytest.mark.parametrize装饰器在AST里是Call节点解析时会把它当成一个动作节点。为了不误导阅读者我检测到装饰器名字包含parametrize时就把参数名和参数值提取出来生成一个“参数化矩阵”节点。在流程图中看参数列表比在代码里挨个翻更直观。第二个是异常分支。with pytest.raises(...)的处理不能只生成一个普通的action节点。我解析出预期的异常类型生成一个专门的“预期异常”节点并用一条不同的分支线连接到后续节点让读者一眼看出这是一个负向场景。第三个是循环场景。for循环想画好关键在“有一条返回边”。PlantUML支持在节点之间画返回线我把循环体包在一个子流程里在循环开始前生成一个判断节点“是否还有下一个数据”是则回退到循环体否则继续向下。这个画法对轮询类测试特别好用比如“等待订单状态变成已支付”。这部分的代码量不小网上开源参考也很多核心原则就一条不要让Rule写死在解析器里把每种语法结构对应哪种节点类型的配置放到单独的一个映射表里后续扩展新语法时只改配置。5. 真实项目中的应用效果5.1 下单流程测试用例的生成全过程拿一个真实的场景演示。某订单系统的下单链路核心场景是“库存充足直接下单成功”。测试代码长这样# tests/test_order.py import requests def test_create_order_stock_enough(): 库存充足时用户下单成功 # 准备数据 prepare_stock(product_id1001, stock10) # 用户登录 login(customer_01, ***) # 进入商品页并加购 open_product_page(product_id1001) add_to_cart(product_id1001, num2) # 提交订单 submit_order(address_id123) # 校验结果 assert response.status_code 200 assert response.json[order_status] CREATED assert response.json[payment_deadline] is not None把这份文件交给脚本python cli.py tests/test_order.py -o output/order_flow.png生成的PlantUML文本大致如下startuml skinparam defaultFontName Microsoft YaHei left to right direction :库存充足时用户下单成功; :prepare_stock; :login; :open_product_page; :add_to_cart; :submit_order; :[response.status_code 200]; :[response.json[order_status] CREATED]; :[response.json[payment_deadline] is not None]; enduml渲染出来的图非常清晰以测试目标为起点依次经过数据准备、登录、浏览、加购、下单最后三个断言校验节点并列收尾。这在评审会上直接就能用不需要额外整理。5.2 分支条件和异常场景的效果对比再展示一个带分支的场景。库存不足时系统应提示库存不足而不允许下单def test_create_order_stock_not_enough(): 库存不足时下单被拦截 prepare_stock(product_id1001, stock1) if stock 2: submit_order(address_id123) assert response.status_code 400 assert response.json[error_code] STOCK_NOT_ENOUGH else: # 正常下单 submit_order(address_id124) assert response.status_code 200脚本会把这段解析成流程图的判断节点“库存是否不足”。左侧是“是”分支走到下单失败断言右侧是“否”分支走正常下单断言。这样测试场景之间的差异一目了然哪些分支覆盖了、哪些没覆盖看图就知道。我顺手统计了一下使用前后的一些数据。以前画一张下单链路的图平均要40分钟改一版需求又要重画一个月光画图就要花掉大半天。现在脚本生成一张图不到10秒整个团队一个月至少能省下小半天的工作量。更重要的是图随时保持最新再也没有人拿旧图出来误导讨论了。5.3 在团队内落地的三个经验工具写好了真正推下去又是另一回事。我总结了三个落地经验。第一个经验是“先解决最痛的场景”。我刚开始没有把整套脚本包装得很完善只拿团队里最复杂的一个支付链路做试点。跑出来的图一对比大家发现比手工画得还清楚后面自然有人问你要工具。从痛点切入比从工具宣讲切入有效得多。第二个经验是“用起来比做完美更重要”。第一版脚本只能处理直线流程一遇到复杂分支就乱。但我没等它完美就拿出来用让大家先熟悉这个工作流再根据反馈逐步把分支、异常、循环这些场景补上。早点暴露在真实场景里比闭门造车靠谱。第三个经验是“把脚本沉淀成公共文档”。脚本用起来了之后我把用法写进团队的测试规范里要求新用例在提交评审时同步附上生成的流程图。这样一来流程图从“可有可无的附件”变成了“评审的必要材料”整个链路就闭环了。6. 踩坑实录常见问题与排查技巧6.1 中文字体乱码问题最先遇到的问题就是中文乱码。PlantUML渲染PNG时如果系统没有合适的字体中文字符会显示成方框。我这个脚本的目标用户是中文测试团队这关不过后面什么都别提。解决思路是给PlantUML指定一个系统里存在的字体。在skinparam里配置defaultFontName我用的是“Microsoft YaHei”。如果你在Linux服务器上跑可能没有这个字体那就在系统里装一个文泉驿或者Noto Sans CJK然后把字体名改掉。这个配置必须写在startuml之后不等号后面是字体显示名不是字体文件名。如果是SVG输出乱码问题会少很多因为SVG是按文本存储的读者端负责渲染字体。所以我的建议是多人协作时优先输出SVG要贴文档或打印时再出PNG。6.2 流程图布局混乱第二个高发问题是布局乱。节点一多PlantUML经常把分支线拉得到处都是阅读体验很糟糕。我试过几个办法。最有效的是前面提到的left to right direction把整体布局从纵向变成横向交叉线变少很多。其次是给不同的分支加“泳道”。PlantUML支持泳道比如按“前端动作”“后端接口”“数据库校验”来分区。泳道能把一组高度相关的节点固定在同一块区域内跨区连线都会变少。对于跨系统联调的测试用例这个功能几乎是必开。还有一个技巧是控制每个节点的文本长度。节点标签太长图也会被拉得很宽。我的做法是超过20个字符的节点描述自动截断并在图下方的注释里补充完整文本这样图面干净信息也不丢。6.3 AST解析常踩的边界情况AST解析看着稳实际用起来也有不少边界情况。这里列举几个我踩过的。第一个是ast.unparse对链式比较输出冗长。比如if 0 stock 10unparse 的结果是0 stock and stock 10在节点里显示会很长。我做了精简逻辑把and连接的比较表达式还原成原来的链式写法读起来舒服很多。第二个是嵌套的 if 结构。Python的elif在AST里会被拆成嵌套的If节点如果处理不当流程图里会出现不必要的嵌套分支。我的处理是递归解析时先判断elif把它合并成一个多分支判断节点而不是层层嵌套。第三个是断言表达式的可读性。assert response.json[order_status] CREATED这种在节点里显示时引号会被转义整体比较难读。我在渲染前做了一步“语义翻译”把常见的方法调用转成自然语言描述。比如response.status_code 200转成“响应状态码为200”阅读者不用再对着代码猜。6.4 工程化和推广中遇到的杂项问题最后说几个工程层面的问题。第一个是依赖Java。PlantUML的本地渲染依赖Java运行时有些机器没装脚本就崩了。我后来在脚本里加了环境检测提示用户缺少Java还是缺少PlantUML jar包省去了一堆排查时间。也可以顺便在项目里提供一个Docker命令把渲染服务容器化团队统一使用。第二个是代码库增量解析。大项目的测试目录有几百个文件全量跑一遍很慢。我加了一个“最近修改检测”的逻辑只对过去一天内修改过的测试文件重新生成流程图并在输出文件名里加上Git的短哈希。这样团队成员改完代码提交时流程图也会自动更新不会出现新旧不匹配的问题。第三个是安全执行。AST解析最忌讳用eval或exec去执行被测代码。我全程只用ast做静态分析不执行任何评测代码避免了被测代码中的恶意逻辑或者副作用引发安全问题。这一点在对接外部代码时尤其重要。第四个是输出格式的统一。不同成员的PlantUML版本不同渲染出来的样式可能略有差异。我把skinparam的配置全部固定在了模板里不依赖个人本地配置这样任何人跑同一份代码出来的图都是一样的风格。做了这么久我个人最深的体会是这套“代码一键生成业务流程图”的Skill价值不只在“快”更在于它倒逼你把流程想清楚。以前手画流程图含糊的地方可以糊弄过去现在断言写得乱、分支分不清、步骤没有注释生成的图就暴露无遗。先把一个历史测试文件丢进去跑一遍你会回来感谢这个脚本。
阅读完成 · 觉得有帮助?
咨询建站