Code Diff 测试覆盖分析工具 --- 开发提测后,自动发现遗漏的测试点
一、背景
痛点
在日常开发测试流程中,你是否遇到过这些问题?
场景一:开发提测后,测试同学凭经验判断测试范围
开发说"我就改了一行代码",但这一行可能影响多个逻辑分支。测试同学需要手动看 Code Diff,凭经验判断哪些功能需要回归。遇到新人或不熟悉业务的情况,很容易遗漏关键场景。
场景二:评审通过的用例是静态的,代码变了它不会自动更新
每次迭代评审通过了一批测试用例,但随着代码多次修改,有些用例已经不适合当前版本了------前置条件变了、接口参数变了、预期结果变了。但没人记得去更新它们。
场景三:Code Diff 会告诉你"改了哪里",但不会告诉你"哪里没测"
Git 的 diff 能清晰地展示每一行代码变更,但它不会告诉你:新增的这个 if 分支有没有对应的测试用例?修改的这个条件判断是不是遗漏了边界值测试?
目标
基于这些痛点,我开发了这个工具,它能做到:
- 自动获取 Git Code Diff,解析出结构化的变更信息
- 分析代码变更的影响域,识别出变更的函数、逻辑分支、调用链
- 与评审通过的测试用例智能比对,自动发现哪些逻辑分支没有被覆盖
- 给出补充建议,告诉你要新增哪些用例、更新哪些用例
二、整体思路
核心流程可以用一句话概括:
"代码改了哪里 → 影响了哪些函数和分支 → 现有用例覆盖了没有 → 没覆盖的给出建议"
这个流程分为 5 个步骤:
提交代码 → 提取 Diff → 分析影响域 → 匹配用例 → LLM 智能分析 → 输出报告
流程图
┌─────────────────────────────────────────────────────────────────────┐
│ 输入:Git Commit / Diff 文件 │
└────────────────────────────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Step 1:DiffParser --- 解析代码差异 │
│ │
│ 输入:git diff 输出 │
│ 输出:结构化变更信息(文件列表、变更行、变更类型) │
│ 示例: │
│ UserService.java modified +7/-1 │
│ Hunk L10: 8 行变更 │
│ [removed] login(String, String) │
│ [added] login(String, String, String) │
└────────────────────────────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Step 2:ImpactAnalyzer --- 分析影响域 │
│ │
│ 输入:Diff 结果 + 源文件 │
│ 输出:变更函数、逻辑分支、调用链、风险等级 │
│ 示例: │
│ 变更函数: login() params_changed=True │
│ 影响分支: captcha != null && captcha.length() != 4 │
│ 调用链: login() → authenticate() │
│ 风险等级: medium │
└────────────────────────────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Step 3:CaseMatcher --- 匹配现有用例 │
│ │
│ 输入:影响域报告 + 用例库 │
│ 匹配策略: │
│ ① 文件路径匹配 --- 用例关联了源文件 │
│ ② 函数名匹配 --- 用例关联了函数 │
│ ③ 标签匹配 --- 用例打了模块标签 │
│ ④ 语义匹配 --- 场景描述包含关键词 │
│ 输出:匹配到的用例列表 + 匹配度 │
└────────────────────────────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Step 4:LLMAnalyzer --- 智能分析覆盖缺口 │
│ │
│ 将 Diff + 影响域 + 用例 打包成 Prompt,发给 LLM │
│ LLM 分析: │
│ ① 现有用例覆盖了哪些逻辑分支? │
│ ② 哪些分支没有覆盖? │
│ ③ 需要补充哪些测试场景? │
│ ④ 哪些现有用例需要更新? │
│ 输出:结构化的分析结果(JSON) │
└────────────────────────────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Step 5:ReportGenerator --- 生成报告 │
│ │
│ 输出格式:Markdown / HTML │
│ 报告内容: │
│ ① 变更概览(文件、行数、风险等级) │
│ ② 影响域分析(函数、分支、调用链) │
│ ③ 用例覆盖分析(已覆盖/部分覆盖/未覆盖) │
│ ④ 建议新增用例(场景、步骤、预期结果) │
│ ⑤ 需更新用例(修改建议) │
└─────────────────────────────────────────────────────────────────────┘
三、项目结构
code_diff_test_reviewer/
├── main.py # 主入口(CLI 命令行)
├── config.py # 配置(Ollama 地址、模型等)
├── requirements.txt # 依赖
│
├── models/ # 数据模型层
│ ├── diff.py # Diff 数据结构
│ ├── impact.py # 影响域数据结构
│ └── test_case.py # 测试用例数据结构
│
├── parsers/ # 解析层
│ └── diff_parser.py # Git Diff 解析器
│
├── analyzers/ # 分析层
│ ├── impact_analyzer.py # 影响域分析器
│ └── llm_analyzer.py # LLM 智能分析器
│
├── matchers/ # 匹配层
│ └── case_matcher.py # 用例匹配器
│
├── reporters/ # 报告层
│ └── report_generator.py # 报告生成器
│
└── examples/ # 示例数据
├── sample_diff.txt # 示例 Diff
└── sample_cases.json # 示例用例库
架构分层说明
┌──────────────────────────────────────────────┐
│ 入口层 (main.py) │
│ 命令行参数解析、流程编排、异常处理 │
├──────────────────────────────────────────────┤
│ 数据模型层 (models/) │
│ 定义核心数据结构,各模块间通过模型通信 │
├──────────────┬───────────────┬───────────────┤
│ 解析层 │ 分析层 │ 匹配层 │
│ diff_parser │ impact_analyzer│ case_matcher │
│ │ llm_analyzer │ │
├──────────────┴───────────────┴───────────────┤
│ 报告层 (reporters/) │
│ Markdown/HTML 报告生成 │
└──────────────────────────────────────────────┘
四、核心代码展示
4.1 数据模型层
数据模型是各模块之间通信的"协议",定义了清晰的数据结构。
Diff 数据结构 (models/diff.py):
python
@dataclass
class LineChange:
line_num: int # 行号
content: str # 行内容
type: str # added / removed / context
@dataclass
class Hunk:
start_line: int # 起始行号
lines: List[LineChange] # 行级变更列表
@dataclass
class FileChange:
file_path: str # 文件路径
change_type: str # added / modified / deleted / renamed
hunks: List[Hunk] # 变更块列表
added_lines: int # 新增行数
removed_lines: int # 删除行数
@dataclass
class DiffResult:
files: List[FileChange] # 变更文件列表
summary: DiffSummary # 变更概览
影响域数据结构 (models/impact.py):
python
@dataclass
class FunctionChange:
func_name: str # 函数名
file_path: str # 文件路径
change_type: str # added / modified / deleted
params_changed: bool # 参数是否变更
@dataclass
class Branch:
file_path: str # 文件路径
line: int # 行号
condition: str # 条件表达式
branch_type: str # if / else / switch / try / catch
is_changed: bool # 是否被修改
@dataclass
class CallChain:
caller_func: str # 调用方函数
callee_func: str # 被调用方函数
is_direct: bool # 是否直接调用
@dataclass
class ImpactReport:
changed_functions: List[FunctionChange]
call_chains: List[CallChain]
affected_branches: List[Branch]
risk_level: str # high / medium / low
测试用例数据结构 (models/test_case.py):
python
@dataclass
class TestCase:
id: str # 用例编号
module: str # 所属模块
scenario: str # 测试场景
precondition: str # 前置条件
steps: str # 测试步骤
expected: str # 预期结果
priority: str # P0 / P1 / P2
tags: List[str] # 标签
source_files: List[str] # 关联源文件
source_functions: List[str] # 关联函数
@dataclass
class AnalysisResult:
covered_branches: List[str] # 已覆盖的分支
uncovered_branches: List[str] # 未覆盖的分支
suggested_new_cases: List[SuggestedCase] # 建议新增用例
need_update_cases: List[CaseUpdate] # 需更新用例
risk_assessment: str # 风险评估
summary: str # 总结
4.2 DiffParser --- 解析 Git Diff
这是整个流程的第一步,也是最基础的一步。它的核心能力是:
- 解析
git diff输出,提取文件级变更 - 提取每个文件的变更块(Hunk)和行级变更
- 计算新增/删除行数
核心代码 (parsers/diff_parser.py):
python
class DiffParser:
def parse(self, diff_text: str) -> Optional[DiffResult]:
"""
解析 diff 文本,返回结构化结果
"""
if not diff_text.strip():
return None
files = []
current_file = None
current_hunk = None
summary = DiffSummary()
for line in diff_text.split("\n"):
# 文件头: diff --git a/file b/file
if line.startswith("diff --git "):
# 保存上一个文件
if current_file and current_hunk:
current_file.hunks.append(current_hunk)
if current_file:
files.append(current_file)
parts = line.split()
b_path = parts[3]
file_path = b_path[2:] if b_path.startswith("b/") else b_path
current_file = FileChange(
file_path=file_path, change_type="modified", hunks=[]
)
continue
# 变更块头: @@ -a,b +c,d @@
hunk_match = re.match(r"^@@ -(\d+),?\d* \+(\d+),?\d* @@", line)
if hunk_match:
if current_file and current_hunk:
current_file.hunks.append(current_hunk)
current_hunk = Hunk(
start_line=int(hunk_match.group(2)), lines=[]
)
continue
# 行级变更
if current_hunk and line.startswith(("+", "-", " ")):
line_type = {"+": "added", "-": "removed", " ": "context"}[line[0]]
current_hunk.lines.append(LineChange(
line_num=current_hunk.start_line + len(current_hunk.lines),
content=line[1:],
type=line_type,
))
# 统计
if line_type == "added":
current_file.added_lines += 1
elif line_type == "removed":
current_file.removed_lines += 1
# context 行不计数,但影响行号
if line_type in ("added", "removed"):
pass # 行号增长由 context 行驱动
# 收尾
if current_file and current_hunk:
current_file.hunks.append(current_hunk)
if current_file:
files.append(current_file)
# 计算 summary
summary.total_files = len(files)
summary.total_added = sum(f.added_lines for f in files)
summary.total_removed = sum(f.removed_lines for f in files)
summary.modified_files = [f.file_path for f in files]
return DiffResult(files=files, summary=summary)
4.3 ImpactAnalyzer --- 影响域分析
这个模块是核心分析能力之一,它需要做到:
- 函数识别:通过正则匹配函数定义,用括号匹配确定函数范围
- 分支提取:识别变更行附近的条件分支(if/else/switch/try-catch)
- 调用链追踪:分析变更函数调用了哪些函数,以及哪些函数调用了它
- 风险评估:根据参数变更、分支数量、调用链长度综合评估风险
函数识别核心逻辑 (analyzers/impact_analyzer.py):
python
class ImpactAnalyzer:
def analyze(self, diff_result: DiffResult, repo_path: str = ".") -> ImpactReport:
"""分析变更影响域"""
report = ImpactReport()
for file_change in diff_result.files:
file_path = os.path.join(repo_path, file_change.file_path)
if not os.path.exists(file_path):
continue
with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
source_lines = f.readlines()
# 收集变更行号
changed_lines = set()
for hunk in file_change.hunks:
for line in hunk.lines:
if line.type in ("added", "removed"):
changed_lines.add(line.line_num)
# 识别变更函数
functions = self._extract_functions(
file_change.file_path, source_lines, changed_lines
)
report.changed_functions.extend(functions)
# 提取变更附近的逻辑分支
branches = self._extract_branches(
file_change.file_path, source_lines, changed_lines
)
report.affected_branches.extend(branches)
# 追踪调用链
if report.changed_functions:
call_chains = self._trace_call_chains(report.changed_functions, repo_path)
report.call_chains.extend(call_chains)
# 评估风险等级
report.risk_level = self._assess_risk(report)
return report
函数范围检测:这是比较有挑战的部分。需要支持 C 风格语言(大括号匹配)和 Python(缩进匹配):
python
def _find_func_end(self, source_lines: List[str], func_start: int) -> int:
"""通过括号匹配找到函数结束行"""
first_line = source_lines[func_start - 1]
brace_count = first_line.count("{") - first_line.count("}")
if brace_count > 0:
# C 风格:用括号匹配
for i in range(func_start, len(source_lines)):
line = source_lines[i]
brace_count += line.count("{") - line.count("}")
if brace_count <= 0:
return i + 1
return len(source_lines)
else:
# Python 风格:检测缩进
# ... 通过缩进级别判断函数结束
4.4 CaseMatcher --- 用例匹配器
匹配策略按照优先级分为 4 种:
python
class CaseMatcher:
def match(self, impact_report, cases) -> MatchedCases:
"""
匹配策略(按优先级):
1. 文件路径匹配:用例标注了关联的源文件
2. 函数名匹配:用例标注了关联的函数
3. 标签匹配:用例标注了模块/功能标签
4. 语义匹配:场景描述中的关键词匹配
"""
for case in cases:
score = 0.0
match_type = ""
# 策略 1:文件路径匹配
for cf in case.source_files:
for changed_file in changed_files:
if cf in changed_file or changed_file in cf:
score += 0.5
match_type = "file_match"
# 策略 2:函数名匹配
for sf in case.source_functions:
if sf in changed_funcs:
score += 0.4
match_type = "function_match"
# 策略 3:标签匹配
for tag in case.tags:
if tag.lower() in changed_tags:
score += 0.15
if not match_type:
match_type = "tag_match"
# 策略 4:语义匹配
if any(func.lower() in case.scenario.lower()
for func in changed_funcs):
score += 0.1
if not match_type:
match_type = "semantic_match"
if score > 0:
# 记录匹配结果
...
4.5 LLMAnalyzer --- 智能分析(核心模块)
这是最核心的模块,它将 Diff 结果、影响域分析、现有用例打包成 Prompt,发给大模型,让大模型分析覆盖缺口。
Prompt 结构:
python
def _build_prompt(self, diff_result, impact_report, matched_cases) -> str:
sections = [
"## 一、代码变更概览",
f"修改文件: {s.total_files} 个, 新增: {s.total_added} 行, 删除: {s.total_removed} 行",
"## 二、详细代码变更",
diff_detail,
"## 三、影响域分析",
"变更函数: ...", "影响分支: ...", "调用链: ...",
"## 四、现有测试用例",
cases_detail,
"## 五、分析指令",
"""请分析:
1. 现有用例是否覆盖了所有逻辑分支?
2. 是否引入了新的边界条件?
3. 是否引入了新的异常路径?
4. 哪些用例需要更新?
5. 请列出需要补充的测试场景
输出格式(JSON):
{
"covered_branches": [...],
"uncovered_branches": [...],
"suggested_new_cases": [
{"scenario": "...", "reason": "...", "priority": "P1", ...}
],
"need_update_cases": [...],
"risk_assessment": "medium",
"summary": "..."
}"""
]
return "\n\n".join(sections)
LLM 调用:使用 OpenAI 兼容 SDK 调用 Ollama 本地模型,支持流式输出:
python
def _call_llm(self, prompt: str) -> str:
collected = []
stream = self.client.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": prompt},
],
temperature=ANALYSIS_TEMPERATURE,
max_tokens=8192,
stream=True,
)
for chunk in stream:
delta = chunk.choices[0].delta if chunk.choices else None
if delta and delta.content:
collected.append(delta.content)
return "".join(collected)
4.6 主入口 --- 流程编排
main.py 是整个流程的编排者,它将 5 个步骤串联起来:
python
def main():
# 解析命令行参数
args = parse_args()
# Step 1: 获取并解析 Diff
diff_parser = DiffParser()
diff_result = diff_parser.parse_from_commit(args.commit_id)
# Step 2: 影响域分析
impact_analyzer = ImpactAnalyzer()
impact_report = impact_analyzer.analyze(diff_result, repo_path=args.repo_path)
# Step 3: 加载并匹配用例
cases = load_cases(args.cases)
case_matcher = CaseMatcher()
matched_cases = case_matcher.match(impact_report, cases)
# Step 4: LLM 智能分析
llm_analyzer = LLMAnalyzer()
analysis_result = llm_analyzer.analyze(
diff_result, impact_report, matched_cases
)
# Step 5: 生成报告
report_gen = ReportGenerator()
report_text = report_gen.generate_markdown(
diff_result, impact_report, matched_cases, analysis_result
)
with open(output_path, "w", encoding="utf-8") as f:
f.write(report_text)
4.7 用例格式
测试用例采用 JSON 格式存储,每个用例关联了源文件和函数名,便于自动匹配:
json
{
"test_cases": [
{
"id": "TC-001",
"module": "用户登录",
"scenario": "用户名密码正确登录成功",
"precondition": "用户已注册",
"steps": "1. 输入用户名\n2. 输入密码\n3. 点击登录",
"expected": "登录成功,跳转首页",
"priority": "P0",
"tags": ["login", "UserService", "authentication"],
"source_files": ["src/services/UserService.java"],
"source_functions": ["UserService.login"]
},
{
"id": "TC-002",
"module": "用户登录",
"scenario": "用户名为空登录失败",
"precondition": "登录页面",
"steps": "1. 用户名留空\n2. 输入密码\n3. 点击登录",
"expected": "提示用户名不能为空",
"priority": "P1",
"tags": ["login", "validation"],
"source_files": ["src/services/UserService.java"],
"source_functions": ["UserService.login"]
}
]
}
五、实际效果演示
场景
假设有一个电商系统,最近一次提交改了 2 个文件:
- UserService.java ---
login()方法新增了验证码参数,register()新增了邮箱格式校验 - OrderService.java ---
createOrder()新增了优惠券参数,金额上限从 10000 调整到 50000
运行命令
bash
python main.py --commit-id HEAD~1 --cases test_cases.json
输出结果
============================================================
Code Diff 测试覆盖分析工具
============================================================
[1/5] 获取代码差异...
变更文件: 2 个 | 新增 30 行 | 删除 4 行
modified: src/services/UserService.java (+14/-1)
modified: src/services/OrderService.java (+16/-3)
[2/5] 分析变更影响域...
变更函数: 6 个
createOrder (params_changed: True)
applyCoupon (params_changed: True)
login (params_changed: True)
register (params_changed: True)
影响分支: 15 个
L6 [if] orderId == null || orderId.isEmpty()
L12 [if] amount > 50000
L16 [if] couponCode != null && !couponCode.isEmpty()
L23 [if] couponCode.equals("SAVE10")
调用链: 24 条
风险等级: medium
[3/5] 加载评审通过用例...
加载用例: 6 个
匹配到: 6 个相关用例
[OK] 已覆盖: 4 个
[WARN] 部分覆盖: 2 个
[MISS] 未覆盖: 0 个
[4/5] LLM 智能分析...
分析结论: 本次变更涉及验证码校验、邮箱格式校验、
优惠券支持等多个新功能,现有用例覆盖不足...
建议新增用例: 3 个
建议更新用例: 1 个
[5/5] 生成报告...
报告已生成: output/report.md
完成!
生成的报告内容
生成的报告包含以下核心部分:
1. 变更概览
| 指标 | 值 |
|---|---|
| 修改文件 | 2 个 |
| 新增行 | 30 行 |
| 删除行 | 4 行 |
| 风险等级 | Medium |
2. 影响域分析
检测到 6 个变更函数,15 个影响的逻辑分支,24 条调用链。其中 login() 和 createOrder() 是参数变更,风险较高。
3. 用例覆盖分析
- 已覆盖 4 个:基础登录、注册、订单场景
- 部分覆盖 2 个:缺少验证码、优惠券相关的分支场景
4. LLM 建议新增的用例
| 编号 | 场景 | 优先级 | 原因 |
|---|---|---|---|
| SUG-001 | 验证码长度不为4位登录失败 | P1 | 新增验证码长度校验逻辑 |
| SUG-002 | 邮箱格式不正确注册失败 | P1 | 新增邮箱格式校验 |
| SUG-003 | 使用优惠券下单金额计算正确 | P1 | 新增优惠券参数和折扣计算 |
5. 需更新的用例
| 用例 | 原因 | 建议修改 |
|---|---|---|
| TC-005 | 订单金额上限从 10000 变更为 50000 | 更新用例中的金额阈值 |
六、使用方式
安装
bash
# 1. 克隆项目
git clone <项目地址>
cd code_diff_test_reviewer
# 2. 安装依赖
pip install -r requirements.txt
# 3. 配置 Ollama(可选,用于 LLM 分析)
# 修改 config.py 中的 Ollama 地址
命令行使用
bash
# 方式1:基于 Commit 分析(在 Git 仓库中运行)
python main.py --commit-id HEAD~1 --cases test_cases.json
# 方式2:基于分支对比
python main.py --diff-branch main --cases test_cases.json
# 方式3:直接指定 Diff 文件(不需要 Git 仓库)
python main.py --diff-file diff.txt --cases test_cases.json
# 方式4:指定输出格式和路径
python main.py --commit-id HEAD~1 --cases cases.json \
--format html -o report.html
# 方式5:过滤文件范围
python main.py --commit-id HEAD~1 --cases cases.json \
--filter "src/services/"
# 方式6:跳过 LLM 分析(仅输出结构化信息)
python main.py --commit-id HEAD~1 --cases cases.json --no-llm
参数说明
| 参数 | 说明 |
|---|---|
--commit-id |
Git Commit ID 或 HEAD~N |
--diff-branch |
对比的目标分支名 |
--diff-file |
直接指定 diff 文件路径 |
--cases / -c |
评审通过用例文件(必填) |
--output / -o |
输出报告路径 |
--format / -f |
报告格式:markdown / html |
--filter |
只分析指定路径的变更文件 |
--repo-path |
Git 仓库路径 |
--no-llm |
跳过 LLM 分析 |
七、技术亮点
1. 多语言函数识别
支持 Java、Python、JavaScript、Go、C++ 等主流语言的函数定义识别。通过正则匹配修饰符 + 返回类型 + 函数名的模式,结合括号匹配/缩进分析确定函数范围。
2. 智能调用链追踪
不仅能分析变更函数本身,还能追踪:
- 向上调用:哪些函数调用了变更函数(影响范围扩散)
- 向下调用:变更函数调用了哪些函数(内部逻辑变更)
3. 多策略用例匹配
4 种匹配策略,从文件路径到语义关键词,确保高匹配率。匹配度评分让测试同学能快速判断哪些用例是强相关、哪些是弱相关。
4. LLM 驱动的智能分析
利用大模型的理解能力,自动化分析"代码变更 vs 测试覆盖"的缺口。相比纯规则匹配,LLM 能理解代码语义,发现更隐蔽的遗漏场景。
5. 灵活的输入方式
支持 3 种输入方式:
- Commit 模式:集成到 CI/CD 流水线,每次提交自动分析
- 分支对比模式:MR/PR 时自动分析
- Diff 文件模式:不需要 Git 仓库,传入任何 diff 文本即可
八、适用场景
场景 1:CI/CD 流水线集成
在 GitLab CI / GitHub Actions 中配置,每次 MR 自动运行:
yaml
# .gitlab-ci.yml
test-coverage-check:
script:
- python main.py --diff-branch main \
--cases test_cases.json \
--output report.html
artifacts:
paths: [report.html]
场景 2:提测前自检
开发提测前,自己跑一遍,看看有没有遗漏的测试点:
bash
python main.py --commit-id HEAD~1 --cases test_cases.json
场景 3:测试用例评审辅助
测试同学在看 MR 时,用工具辅助分析,快速定位需要补充的测试场景。
场景 4:新人培训
新人不熟悉业务,看 Code Diff 很难判断测试范围。用工具辅助分析,快速了解变更影响。
九、总结
这个工具的核心价值在于:把"代码变更"和"测试覆盖"之间的鸿沟自动化填补。
以前,测试同学需要手动看 Code Diff,凭经验判断哪些功能需要测试。现在,工具自动分析变更影响域,与测试用例智能比对,精准定位遗漏的测试场景。
以前,评审通过的用例是静态的,代码变了也不会有任何提示。现在,工具会自动检测哪些用例需要更新,给出具体的修改建议。
整个工具基于 Python 开发,轻量易用,可以灵活集成到现有的 CI/CD 流水线中。本文配合 Ollama 本地大模型,如果其他同学公司有采购的大模型也可以接入。
本文为 CSDN 分享文档,如需了解更多细节,欢迎留言交流。