☰
代码一键生成业务流程图:用AST与Mermaid打造测试提效Skill
2026/10/20 15:15:41 网站建设 项目流程

接手一个没有文档的老模块,是测试工程师最头疼的事之一。我的习惯不是对着源码从头读到尾,而是先让脚本把代码里的函数调用关系抽出来,一键生成业务流程图,再基于这张图去梳理测试路径和设计用例。这套做法如果只是临时跑一次,价值有限;真正好用的是把它沉淀成一个可复用的“Skill”,以后每个项目都能直接调。今天就把这个“代码一键生成业务流程图Skill”从设计思路到落地实现完整拆开讲。

这个Skill解决的是测试工作中一个很实际的矛盾:代码是系统的真实状态,但业务流程图往往只停留在需求文档里,两者经常对不上。测试工程师需要的是“从代码反推出来的当前业务链路”,而不是理想化的流程描述。文章适合正在做接口测试、白盒测试、或者刚接手陌生模块的测试同学,也适合想给团队做效率工具的开发者。下面我会从静态分析选型、脚本实现、Skill封装到问题排查,给你一套可以直接抄作业的方案。

1. 核心思路:为什么测试工程师需要代码生成流程图

1.1 读代码和画流程图的现实痛点

代码里最让测试头疼的不是语法,而是调用关系。一个接口处理器,表面上只有几行逻辑,背后可能调了十几个service方法,每个service方法又有自己的依赖。如果靠人肉从上往下读,很容易在函数跳转中迷失方向,更糟糕的是:你读到的顺序是代码文件顺序,而真正执行业务的顺序是调用链顺序,两者不一致时,测试设计就会漏路径。

画流程图也一样。先看需求文档画一张预期流程,再对照代码发现实现和文档不一致,这时候改图还是改理解?很多测试选择改图,结果画出来的东西既不是需求的流程,也不是代码的流程,变成一张“想象中的流程图”。用代码生成流程图,至少能保证图中的每一个节点都是真实存在的函数,每一条边都来自实际的调用点。它不是替代人去理解业务,而是把理解范围先限定在真实代码结构里。

从测试用例设计的角度来看,流程图最大的价值是帮助识别路径覆盖。if分支、异常处理、循环回边,如果没被画出来,设计用例时就很容易忽略。把代码调用图作为测试设计底稿,再用业务知识去补充场景,比直接空想测试路径要扎实得多。

1.2 这个Skill能干什么、不干什么

先说能干的:给定一个源码目录和一个入口函数,Skill可以自动抽取这个入口往下若干层调用关系,生成一张可缩放的业务流程图;可以指定深度、按模块过滤、给节点补充人类可读标签;还能把结果输出成Mermaid或PlantUML文本,方便写进测试报告和接口文档。

它也解决一个很实际的问题:当你手上只有一个模块名,不知道怎么找测试入口时,Skill可以先扫描整个项目的函数定义,把疑似入口的controller/service/接口方法列出来,再由你确认。确认后一键出图,几分钟内就能对一个陌生模块建立整体印象。

但要注意,它不擅长的事情很多。代码生成的是“静态调用关系图”,不是运行时真实的数据流图。比如反射调用、根据变量名动态调用函数、通过消息队列异步解耦的链路,静态分析工具是抓不到的。也不要指望它自动判断业务是否合理,它只负责把代码长什么样画出来,业务判断还是得靠自己。

注意:这个Skill的定位是“提速”而不是“替代”。尤其在做测试方案评审或者跟开发对需求时,那张图是沟通工具,但最终测试结论必须以真实行为为准。

2. 工具选型与Skill设计原则

2.1 静态分析方案怎么选

要做代码调用关系抽取,常见有三条路。第一种是用IDE自带的图功能,比如常见商业IDE里右键某个方法就能看到调用图。优点是不用写代码,缺点是没法批量处理整个目录,也无法定制过滤规则,更没法集成到自动化流程里,对我这种想做成Skill的人来说不够灵活。

第二种是用专门静态分析工具,像基于语法树解析器的开源方案,能够跨语言处理。这类工具功能很强,但学习曲线比较陡,配置复杂,而且对测试团队来说往往太重了。我们要的只是一个“能抽调用关系”的脚本,不需要做全量数据流分析。

第三种就是自己写解析脚本,用Python标准库ast模块去解析源码。这个方法最契合Skill思路,因为它轻量、可控、易于打包。ast可以把代码变成语法树,我们只关心两类节点:函数定义节点和函数调用节点。遍历一遍就能拿到“谁调用了谁”的关系列表。对多数测试场景来说,这种信息粒度已经足够了。

我自己最终选了第三种:Python脚本做解析,配合规则过滤。选择它不是因为别的方案不好,而是因为测试团队日常要处理的项目往往语言杂、结构乱,一个脚本加一个配置文件的方案,最容易沉淀成通用工具。后面如果想支持Java、Go,可以再加一层tree-sitter之类的通用解析器,但核心的流程设计不变。

2.2 流程图表达语言怎么选

解析完之后,需要用一种文本形式表达流程图,再渲染成图片。目前主流的选择有三种:Mermaid、PlantUML和Graphviz的DOT语言。我把它们放在一起做过对比,各有明显倾向。

方案优点缺点适合场景
Mermaid语法简单、Markdown友好、渲染快复杂图大量节点时布局一般嵌入测试报告、技术文档、AI对话
PlantUML支持泳道图/时序图,表达能力更强语法稍重,需要Java环境需要表达角色分工的业务流程
Graphviz/DOT自动布局算法强大,适合复杂调用图语法原始,需要定制样式全量调用图、超过100个节点的图

对测试工程师来说,我首选Mermaid。原因是它的文本形态非常容易放进测试用例附件里,也容易用脚本拼装。哪怕不渲染成图片,直接把Mermaid代码贴到支持Markdown渲染的地方,就能看到图形。另外,Mermaid的节点支持中文标签,配合后续的业务语义增强,出来的图更像测试需要的“业务流程图”而不是“架构依赖图”。

2.3 Skill包的目录与交互约定

“Skill”这个词听起来玄,其实本质就是一个约定好的工作目录:里面放脚本、说明文档、配置和输出位置。用的时候,把源码路径和入口函数传给这个目录下的脚本,它按固定格式返回调用关系JSON,再渲染成流程图。

我建议把Skill设计成三层交互约定。第一层是输入,要明确告诉使用方必须提供源码目录和入口函数名,可选提供深度、过滤关键字。第二层是处理,脚本执行分析、裁剪、生成文本图三件事。第三层是输出,返回两个文件:一个是结构化的关系JSON,方便后续二次处理;另一个是Mermaid格式的流程图文本。如果配合AI助手使用,还要加一层提示词模板,让AI不要自己脑补函数名,只依据脚本输出整理业务标签。

这样的设计好处是每个环节都能单独调试。脚本出了问题,单独跑脚本看输出;标签不友好,只调整提示词;图太乱,只改过滤规则。拆分开以后,这个Skill不绑定任何特定的编辑器或工具链,命令行能跑,AI插件能接,测试平台也能集成。

3. 实操:从源码到业务流程图的完整链路

3.1 环境准备与目录结构

先准备一个最简单的运行环境。Python 3.9以上即可,不需要装第三方库,解析和生成Mermaid文本用标准库就能完成。如果要渲染成图片,需要额外准备Mermaid命令行渲染器,或者干脆先把文本存下来,放到任何支持Mermaid渲染的地方再转图片。

推荐的项目结构是这样的:

skill-code2flow/ ├── skill.md # Skill说明文档,告诉使用者怎么调用 ├── config.yaml # 过滤规则、深度默认值、关键词配置 ├── scripts/ │ └── analyze_code.py # 核心解析脚本 ├── output/ │ ├── graph.json # 调用关系JSON │ └── flowchart.mmd # Mermaid格式流程图 └── examples/ └── demo_project/ # 模拟待分析项目

实际使用中,建议把脚本放在独立目录里,不要和被测项目混在一起。这样Skill本身不会污染被测代码目录,也方便用同样的脚本分析多个项目。config.yaml主要放两类配置:一类是忽略哪些路径,比如第三方库目录;另一类是入口候选函数名的关键字,比如handler、service、action等。

3.2 用AST提取函数调用关系

核心脚本的第一件事是把源码文件逐个解析成语法树,然后遍历函数定义和函数调用节点。先看一个最简版本的理解框架:

import ast from pathlib import Path def extract_calls(project_dir): relations = [] for py_file in Path(project_dir).rglob("*.py"): try: tree = ast.parse(py_file.read_text(encoding="utf-8"), filename=str(py_file)) except SyntaxError: continue analyzer = CallRelationVisitor() analyzer.visit(tree) for caller, callee, lineno in analyzer.calls: relations.append({ "caller": caller, "callee": callee, "lineno": lineno, "file": str(py_file), }) return relations

上面这段代码的逻辑很直接:遍历项目里所有Python文件,逐个用ast.parse解析,然后交给一个自定义Visitor去收集调用关系。注意,异常处理很重要,测试项目里经常会有临时文件、生成代码、或者注释里出现的非标准内容,单个文件解析失败不应该中断整个流程。

再写出CallRelationVisitor,它需要处理普通函数、异步函数、类方法以及常见的调用写法。

class CallRelationVisitor(ast.NodeVisitor): def __init__(self): self.calls = [] self.current_func = None def visit_FunctionDef(self, node): old = self.current_func self.current_func = node.name self.generic_visit(node) self.current_func = old visit_AsyncFunctionDef = visit_FunctionDef def visit_Call(self, node): if self.current_func is None: self.generic_visit(node) return callee = None if isinstance(node.func, ast.Name): callee = node.func.id elif isinstance(node.func, ast.Attribute): # 处理 obj.method() 和 self.method() callee = node.func.attr if callee: self.calls.append((self.current_func, callee, node.lineno)) self.generic_visit(node)

这里有个细节:如果方法是ClassA.method_a调用了self.helper(),我们抓到的被调函数是helper而不是ClassA.helper,因为静态AST里只能拿到属性名。处理方式是先把这种调用关系存下来,后面生成图时统一按短名称作为节点。如果项目里有重名方法,就需要结合类名做去重。大部分测试场景下,按函数短名画图已经够用,但如果遇到同名函数过多导致节点错乱,再升级解析逻辑也不难。

注意:不要试图在第一步就把类继承关系全部解决。先把“函数到函数”的链路画出来,遇到确实分不清的再补人工映射,否则脚本会复杂到难以维护。

3.3 按入口裁剪业务链路

如果把项目里所有函数调用关系都画出来,结果大概率是一张密密麻麻的全家福。业务流程图应该关注“某个业务入口往下发生了什么”,所以必须做裁剪。我一般用两种策略:第一个是入口裁剪,指定从哪个函数开始;第二个是深度限制,默认只画5层以内的调用。

入口裁剪可以用广度优先遍历实现,从入口函数出发,沿着调用边往下走,只保留能走到的节点和边。

def build_graph_from_entry(relations, entry, max_depth=5): call_map = {} for rel in relations: call_map.setdefault(rel["caller"], []).append(rel["callee"]) graph = {} queue = [(entry, 0)] visited = set() while queue: node, depth = queue.pop(0) if node in visited or depth > max_depth: continue visited.add(node) callees = [c for c in call_map.get(node, []) if not is_external(c)] graph[node] = callees for callee in callees: queue.append((callee, depth + 1)) return graph

这段代码里有一个is_external函数负责判断是否属于外部调用。通常外部库的函数名会出现在调用关系里,比如print、logger.info、str之类。一个实用的过滤方式是维护一个忽略名单,或者在提取调用关系时只保留项目内已定义函数。两种方式结合效果更好:先收集所有函数定义集合,再过滤调用关系,只保留调用关系两侧都在定义集合里的边。

裁剪之后,用graph[node] = callees的方式保存每个节点可以继续展开的下一层节点。这样做还有一个好处:即使depth限制比较深,图里也只保留业务主路径,不会把无关函数带进来。

3.4 输出并渲染业务流程图

裁剪完成的graph对象,下一步转换成Mermaid文本。Mermaid的flowchart语法非常直观,把每个调用关系写成一行“A --> B”即可。

def to_mermaid(graph): lines = ["flowchart TD"] for caller, callees in graph.items(): for callee in callees: lines.append(f" {caller} --> {callee}") return "\n".join(lines)

这个函数虽然短,但建议单独抽出来,因为后续你要在节点标签上做很多花样。默认情况下节点名是函数名,比如get_unpaid_orders。如果直接展示,图能看但不够友好。后面我会讲到怎么给节点换标签,生成更接近业务流程图的版本。

生成Mermaid文本后,可以保存成.mmd文件,然后通过Mermaid命令行工具渲染成PNG或SVG。我常用的是先在编辑器里预览,确认结构没问题后再批量渲染,避免每次调整都浪费时间。测试团队内部如果统一使用Markdown写作平台,直接贴Mermaid代码块是最快捷的分享方式。

3.5 把流程封装成Skill模板

脚本能跑通只是第一步,真正让它变成“一键”还得封装。我通常在Skill目录里放一个skill.md,内容写清楚使用方法和约束。这个文件也是给AI助手看的提示词模板,如果团队用了对话式辅助工具,可以直接把它作为技能说明加载。

skill.md的核心内容不需要多复杂,但要写清三个点:

# Skill:代码业务流程图生成 触发方式:用户提供源码目录和入口函数名,例如“分析order模块的create_order入口”。 执行步骤: 1. 运行 scripts/analyze_code.py -s <源码目录> -e <入口函数名> -d <最大深度> 2. 读取 output/graph.json 3. 依据 graph.json 生成 Mermaid flowchart 文本 4. 用函数注释和命名规范,将节点名改写为业务可读标签 5. 输出 Mermaid 代码,附带关键路径说明 强制约束: - 所有函数名必须来自 graph.json,禁止凭空增加未出现的节点 - 如果入口函数不存在,必须提示用户检查函数名 - 节点标签可以推测,但推测必须附在函数名后面的括号里

如果直接用命令行,不需要AI,那么让使用者记python3 scripts/analyze_code.py ...就行。如果需要支持更多项目,脚本里加--language参数,再预留Java、Go解析器的接口。Skill的核心价值在于:入口、裁剪、过滤、输出这些琐碎逻辑被沉淀和复用,测试人员不用每次重新写。

4. 让流程更贴近业务语义

4.1 用注释和命名给节点换标签

函数名是给机器看的,业务流程图是给人看的。比如calc_discount_by_level,中文业务语义可能是“按用户等级计算折扣”。如果图里直接显示函数名,开发能看懂,但测试和产品不一定能在第一时间反应过来。我的处理方式是把函数名和docstring一起输出,再通过一套“命名转换+AI润色”的步骤生成节点标签。

具体操作分两步。第一步是脚本增强,在提取调用关系时,把每个函数的docstring摘要也记录下来。比如只取docstring的第一行或前20个字。第二步是提供一张“标签映射表”,Skill在生成Mermaid时,把节点名替换成可读标签,同时保留原名:

graph TD PlaceOrder["下单入口"] --> CheckStock["检查库存(handle_stock)"] PlaceOrder --> CalcPrice["计算金额(calc_amount)"]

这样做的价值很直接:测试人员在评审用例时,可以先看中文标签快速理解主流程,遇到可疑节点再看函数原名去定位代码。映射表不用手工维护,Skill可以根据命名和注释自动生成初始版本,再由人工调整。注意不要让自动转换过度发挥,比如遇到动词缩写或者无意义的内部函数,强行翻译反而会造成误导。

4.2 捕获分支与异常,画出判断节点

测试最关注的是判断和异常分支,而普通的函数调用图把这些信息丢掉了。要让生成结果更接近“业务流程图”,必须把函数内部的if条件、try异常处理也提取出来。AST里同样能拿到这类节点,可以在Visitor里增加对If、Try、With等节点的处理。

一个常规做法是给每个函数追加一个伪节点,用来表示控制流。例如函数refund_order内部有一个if order.status == "paid"的判断,我们可以把条件表达式从AST节点里提取出关键字和比较对象,在流程图中生成一个菱形判断节点,然后挂在函数节点下方。这样整张图就开始呈现“业务流程”的味道,而不只是“函数依赖图”。

异常处理也可以类似处理。遇到try节点时,把异常类型和对应的handler函数名作为一条边,并打上“异常处理”的标记。测试用例设计时,你一眼就能看出哪些异常分支还没被覆盖。这个增强会引入较多代码量,建议在脚本里做成可选开关,默认关闭,需要时通过--with-branches开启,以免干扰常规的调用关系图。

4.3 模块聚合与子图拆分

一个大型模块如果入口不变,画出来的图很快就超出屏幕范围。这时需要用子图来聚合。Mermaid支持在流程图中声明子图,我们可以在生成图时,根据函数所在文件或模块路径,把同类节点聚合到同一个子图里。

脚本生成的graph.json里本来就记录了每个函数所在的源代码文件路径,按上一级目录名做聚合是很自然的事。例如,订单模块里的service、repository、client分别归入三个子图。这种拆分对测试的意义在于:可以从“哪个模块的代码逻辑最复杂”来判断测试重点,也可以在做回归分析时快速圈出受影响模块。

聚合粒度不建议太细,否则一个函数一个子图,反而打乱了主流程。我一般只对第二层目录做聚合,节点数量超过15个的函数单独保留在主图上。如果一张图超过40个节点,我会建议看的人按入口拆成多张子图去看,而不是追求一张图覆盖所有情况。

5. 常见问题与排查技巧实录

5.1 解析结果不准或不全

最常遇到的问题是脚本跑出来“缺边”,明明代码里调用了某函数,图里却没有。我排查的顺序一般是:先看目标函数是否定义在被扫描目录内,因为脚本默认只保留项目内定义的函数调用,外部模块的调用会被主动过滤。第二看是不是类方法通过self.xxx()调用,我们只取了方法名,如果同名方法较多,可能被当成另一个节点。第三看代码里是不是用了动态调用,比如getattr(obj, method_name)(),这种静态AST分析不到,需要在最终图上手动补一条虚拟边。

还有一个很常见的是装饰器问题。被@staticmethod或自定义装饰器包过的函数,AST里仍然会进入FunctionDef节点,所以普通装饰器不影响。但如果用了语法糖比如Java注解处理框架,或者Python把函数放进容器再取出来调用,脚本就会识别不出来。处理方法是接受这个局限,在Skill说明里注明“动态调度需人工补充”。

5.2 渲染和可读性问题

图能生成但渲染出来很乱,是第二个高频问题。节点太多时建议缩小深度,优先检查入口选择是否正确。如果入口选错,比如选了一个工具类函数而不是业务接口函数,整个图就会歪。判断入口是否正确的标志很简单:图里应该能看到核心业务步骤,而不是一堆base方法。

渲染中文乱码的问题也不少见。Mermaid渲染时如果不指定字体,中文可能显示为方框。建议在生成图片时配置中文字体参数。如果只是粘贴到Markdown文档里,大多数在线渲染平台已经处理好中文字体,不用额外操心。

如果想让图更好读,还有一个技巧:给“关键入口节点”和“异常处理节点”使用不同颜色。这需要在生成Mermaid文本时给节点加样式声明,虽然脚本复杂度增加一点,但测试评审时非常实用。

5.3 提升复用性的经验

我用这个Skill跑了几个项目之后,最大的感触是:每个团队都有一套自己的命名规范和模块划分,所以Skill必须配置化,而不是硬编码。建议把过滤名单、入口关键字、深度默认值都放到配置文件里,不要埋在脚本中。这样换一个项目,只需调整配置,不需要改代码。

还有一个经验是:一定要保留graph.json。很多时候我们不只是要看最新版的图,还要对比两个版本之间的调用关系变化。比如这次迭代新增了一个中间校验步骤,如果只有最终图片,对比非常困难。保留结构化JSON后,可以写一个diff脚本,快速找出新增、删除和移动的节点。这在回归测试评估影响范围时简直太香了。

最后,Skill不要一个人捂着用。在一个测试小团队里,可以让一个人先维护脚本和配置,其他人只负责提供入口函数名和使用反馈。每次遇到脚本解析错误,就让维护者补充规则。用不了几次,这个Skill就会长成团队真正需要的样子,而不是一个只能生成“函数调用图”的玩具。

我在实际使用中体会到,这个Skill最有价值的时刻不是第一次跑出图的时候,而是你拿着图去跟开发对需求、跟产品确认逻辑的时候。沟通双方终于可以指着同一个图说“这里有个分支没覆盖”“这一步顺序不对”。刚开始别追求一张图包含所有信息,控制入口、控制深度、控制过滤范围,让图先可用,再慢慢加语义。等你用顺手了,就会发现它成了测试开工前必备的前置动作。

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

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

立即咨询