回归测试指南
June 3, 2026 · View on GitHub
本文档为tree-sitter-analyzer项目提供全面的回归测试指南,帮助开发者理解和使用Golden Master方法进行回归测试。
📋 目录
回归测试概述
什么是回归测试?
回归测试是确保代码更改不会破坏现有功能的测试。它们验证:
- API向后兼容性: 旧API仍然工作
- 输出格式稳定性: 输出格式保持一致
- 功能完整性: 现有功能继续正常工作
为什么需要回归测试?
- 防止破坏性变更: 捕获意外破坏现有功能的更改
- 确保向后兼容: 验证新版本与旧版本兼容
- 提高信心: 在发布前确保代码质量
- 自动化验证: 通过CI/CD自动运行,节省时间
回归测试类型
| 类型 | 描述 | 示例 |
|---|---|---|
| 格式回归 | 输出格式保持一致 | Python分析输出格式不变 |
| API回归 | API接口保持兼容 | 旧API调用仍然工作 |
| 兼容性回归 | 跨版本兼容 | v1.6配置文件在v1.7中工作 |
Golden Master方法
什么是Golden Master?
Golden Master是已知正确的输出快照,用作回归测试的参考标准。它记录了系统在特定状态下的预期行为。
Golden Master工作原理
1. 创建Golden Master
↓
2. 代码变更
↓
3. 运行回归测试
↓
4. 比较当前输出与Golden Master
↓
5. 如果匹配 → 测试通过
如果不匹配 → 测试失败
Golden Master文件结构
tests/golden_masters/
├── python_sample_full.txt # Python完整格式输出
├── python_sample_compact.txt # Python紧凑格式输出
├── java_sample_full.txt # Java完整格式输出
├── javascript_sample_full.txt # JavaScript完整格式输出
└── toon/ # Toon格式输出
├── python_sample_toon.toon
├── java_sample_toon.toon
└── javascript_sample_toon.toon
创建Golden Master
方法1: 手动创建
# 1. 运行分析并保存输出
uv run python -m tree_sitter_analyzer analyze \
examples/sample.py \
--format full > output.txt
# 2. 验证输出正确性
cat output.txt
# 3. 复制到golden_masters目录
cp output.txt tests/golden_masters/python_sample_full.txt
方法2: 自动创建
# 使用pytest的--update-golden-masters标志
uv run pytest tests/regression/test_format_regression.py \
--update-golden-masters
# 这将自动更新所有Golden Master文件
方法3: CI/CD自动更新
# .github/workflows/regression-tests.yml
- name: Update Golden Masters
if: inputs.update-golden-masters == true
run: |
uv run pytest tests/regression/test_format_regression.py \
--update-golden-masters
Golden Master最佳实践
- 版本控制: Golden Master文件应该提交到版本控制
- 明确命名: 使用描述性文件名(如
python_sample_full.txt) - 文档化: 在测试文件中记录Golden Master的用途
- 定期审查: 定期检查Golden Master是否仍然有效
- 变更日志: 记录Golden Master变更的原因
# 测试文件中的文档
def test_python_format_stability():
"""测试Python格式输出的稳定性。
Golden Master: tests/golden_masters/python_sample_full.txt
创建日期: 2025-01-01
最后更新: 2025-01-15
更新原因: 添加了新的输出字段
"""
# 测试代码
回归测试添加流程
步骤1: 识别需要回归测试的功能
考虑以下问题:
- 这个功能是否影响用户可见的输出?
- 这个功能是否影响API接口?
- 这个功能是否影响配置文件格式?
- 这个功能是否影响数据序列化?
如果答案是"是",则需要回归测试。
步骤2: 创建测试用例
格式回归测试示例
"""测试格式输出回归。"""
import pytest
from pathlib import Path
from tree_sitter_analyzer.core import analyze_code_structure
class TestFormatRegression:
"""格式输出回归测试。"""
@pytest.mark.regression
def test_python_format_stability(self, sample_python_file):
"""测试Python格式输出的稳定性。
验证Python代码的完整格式输出保持稳定。
Golden Master: tests/golden_masters/python_sample_full.txt
"""
# 执行分析
result = analyze_code_structure(
sample_python_file,
format_type="full"
)
# 加载Golden Master
golden_master_path = Path(
"tests/golden_masters/python_sample_full.txt"
)
with open(golden_master_path, 'r', encoding='utf-8') as f:
expected_output = f.read()
# 验证一致性
assert result == expected_output, (
f"Format output changed. "
f"Expected:\n{expected_output}\n\n"
f"Got:\n{result}"
)
@pytest.mark.regression
def test_java_format_stability(self, sample_java_file):
"""测试Java格式输出的稳定性。"""
result = analyze_code_structure(
sample_java_file,
format_type="full"
)
golden_master_path = Path(
"tests/golden_masters/java_sample_full.txt"
)
with open(golden_master_path, 'r', encoding='utf-8') as f:
expected_output = f.read()
assert result == expected_output
@pytest.mark.regression
def test_toon_format_stability(self, sample_python_file):
"""测试Toon格式输出的稳定性。"""
result = analyze_code_structure(
sample_python_file,
format_type="toon"
)
golden_master_path = Path(
"tests/golden_masters/toon/python_sample_toon.toon"
)
with open(golden_master_path, 'r', encoding='utf-8') as f:
expected_output = f.read()
assert result == expected_output
API回归测试示例
"""测试API回归。"""
import pytest
from tree_sitter_analyzer.core.request import AnalysisRequest
class TestAPIRegression:
"""API向后兼容性测试。"""
@pytest.mark.regression
def test_old_api_compatibility(self):
"""测试旧API创建方式仍然工作。"""
# 旧方式(v1.0)
request = AnalysisRequest(
file_path=Path("test.py"),
language="python",
format_type="full"
)
# 验证所有字段都存在
assert hasattr(request, 'file_path')
assert hasattr(request, 'language')
assert hasattr(request, 'format_type')
assert hasattr(request, 'include_details')
@pytest.mark.regression
def test_new_api_parameters_accepted(self):
"""测试新参数被接受。"""
# 新方式(v1.5+)
request = AnalysisRequest(
file_path=Path("test.py"),
language="python",
format_type="full",
include_details=True, # 新参数
include_complexity=True, # 新参数
include_dependencies=True # 新参数
)
# 验证新参数被接受
assert request.include_details is True
assert request.include_complexity is True
assert request.include_dependencies is True
兼容性回归测试示例
"""测试跨版本兼容性。"""
import pytest
from pathlib import Path
import yaml
class TestCrossVersionCompatibility:
"""跨版本兼容性测试。"""
@pytest.mark.regression
def test_config_file_compatibility(self):
"""测试配置文件向后兼容性。"""
# v1.6配置文件
config_v1_6 = {
"language": "python",
"queries": [
{"name": "classes", "query": "(class_definition) @class"}
]
}
# 应该仍然工作
from tree_sitter_analyzer.core.config import load_config
config = load_config(config_v1_6)
assert config is not None
assert config["language"] == "python"
assert len(config["queries"]) == 1
@pytest.mark.regression
def test_missing_fields_handled(self):
"""测试缺失字段被正确处理。"""
# 不完整的配置文件
incomplete_config = {
"language": "python"
# 缺少"queries"字段
}
from tree_sitter_analyzer.core.config import load_config
config = load_config(incomplete_config)
# 应该使用默认值
assert config is not None
assert config["queries"] == []
步骤3: 添加测试标记
使用@pytest.mark.regression标记回归测试:
@pytest.mark.regression
def test_regression_example():
"""这是一个回归测试。"""
pass
步骤4: 运行回归测试
# 运行所有回归测试
uv run pytest tests/ -m regression
# 运行特定回归测试
uv run pytest tests/regression/test_format_regression.py
# 运行带详细输出的回归测试
uv run pytest tests/ -m regression -v
# 运行带覆盖率的回归测试
uv run pytest tests/ -m regression --cov=tree_sitter_analyzer
步骤5: 验证测试
确保测试:
- 通过: 在当前代码上运行并通过
- 失败: 在有问题的代码上运行并失败
- 有意义: 失败消息清晰且有帮助
回归测试维护指南
何时更新Golden Master
更新Golden Master的情况:
- 有意更改: 功能性更改需要新输出格式
- Bug修复: 修复了导致错误输出的bug
- 新功能: 添加了新的输出字段
- 重构: 代码重构改变了输出顺序
不更新Golden Master的情况:
- 代码清理: 不影响输出的代码改进
- 性能优化: 不影响输出的性能改进
- 文档更新: 仅文档更改
更新Golden Master的流程
本地更新
# 1. 更新Golden Master
uv run pytest tests/regression/test_format_regression.py \
--update-golden-masters
# 2. 验证更改
git diff tests/golden_masters/
# 3. 提交更改
git add tests/golden_masters/
git commit -m "chore: update golden masters for new feature"
# 4. 推送到远程
git push
CI/CD更新
# .github/workflows/regression-tests.yml
golden-master-update:
name: Update Golden Masters
runs-on: ubuntu-latest
if: github.event_name == 'workflow_dispatch' &&
inputs.update-golden-masters == true
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Update Golden Masters
run: |
uv run pytest tests/regression/test_format_regression.py \
--update-golden-masters
- name: Commit and push changes
run: |
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git add tests/golden_masters/
git commit -m "chore: update golden masters [skip ci]"
git push
Golden Master变更文档
每次更新Golden Master时,记录变更:
## Golden Master变更日志
### 2025-01-15
**原因**: 添加了新的输出字段`complexity`和`dependencies`
**影响文件**:
- `tests/golden_masters/python_sample_full.txt`
- `tests/golden_masters/java_sample_full.txt`
- `tests/golden_masters/toon/python_sample_toon.toon`
**变更详情**:
- 在每个类元素后添加了`complexity`字段
- 在每个方法元素后添加了`dependencies`字段
- 格式保持不变,只是添加了新字段
**审查者**: @username
回归测试失败处理
当回归测试失败时:
-
分析失败原因:
- 是有意更改吗?
- 是意外bug吗?
- 是格式问题吗?
-
如果是无意更改:
- 修复导致失败的代码
- 重新运行测试验证修复
-
如果是有意更改:
- 更新Golden Master
- 记录变更原因
- 提交更新
-
如果是格式问题:
- 检查代码格式化
- 运行
uv run black . - 重新运行测试
回归测试性能优化
如果回归测试运行缓慢:
-
使用pytest缓存:
uv run pytest tests/ -m regression --cache-clear -
并行运行:
uv run pytest tests/ -m regression -n auto -
选择性运行:
# 只运行格式回归测试 uv run pytest tests/regression/test_format_regression.py # 只运行API回归测试 uv run pytest tests/regression/test_api_regression.py
常见问题解答
Q1: Golden Master应该包含在版本控制中吗?
A: 是的。Golden Master文件应该提交到版本控制中,因为它们是测试的参考标准。
Q2: 如何处理小的格式差异(如空格)?
A: 使用规范化比较:
def test_with_normalization():
"""使用规范化比较的测试。"""
result = analyze_code_structure(file_path, format_type="full")
with open(golden_master_path, 'r') as f:
expected_output = f.read()
# 规范化比较(忽略空格和换行)
assert result.strip() == expected_output.strip()
Q3: 如何测试多个输出格式?
A: 为每个格式创建单独的测试:
@pytest.mark.parametrize("format_type,golden_master", [
("full", "python_sample_full.txt"),
("compact", "python_sample_compact.txt"),
("csv", "python_sample.csv"),
])
def test_multiple_formats(self, format_type, golden_master):
"""测试多种输出格式。"""
result = analyze_code_structure(file_path, format_type=format_type)
with open(f"tests/golden_masters/{golden_master}", 'r') as f:
expected_output = f.read()
assert result == expected_output
Q4: 如何处理动态输出(如时间戳)?
A: 在比较前替换动态值:
def test_with_dynamic_values():
"""处理动态值的测试。"""
result = analyze_code_structure(file_path)
with open(golden_master_path, 'r') as f:
expected_output = f.read()
# 替换动态值
import re
result_normalized = re.sub(
r'timestamp: \d+',
'timestamp: <TIMESTAMP>',
result
)
expected_normalized = re.sub(
r'timestamp: \d+',
'timestamp: <TIMESTAMP>',
expected_output
)
assert result_normalized == expected_normalized
Q5: 如何调试回归测试失败?
A: 使用详细的失败消息:
def test_with_detailed_failure():
"""带有详细失败消息的测试。"""
result = analyze_code_structure(file_path)
with open(golden_master_path, 'r') as f:
expected_output = f.read()
if result != expected_output:
# 找出差异
import difflib
diff = difflib.unified_diff(
expected_output.splitlines(keepends=True),
result.splitlines(keepends=True),
fromfile='golden_master',
tofile='actual'
)
# 提供详细的失败消息
raise AssertionError(
f"Output differs from Golden Master:\n"
f"{''.join(diff)}\n\n"
f"To update Golden Master, run:\n"
f"uv run pytest tests/regression/test_format_regression.py "
f"--update-golden-masters"
)
Q6: 回归测试应该运行多频繁?
A: 回归测试应该在每次代码更改时运行:
- Pull Request: 自动运行
- Push to main: 自动运行
- Scheduled: 每日运行(可选)
- Manual: 按需运行
Q7: 如何处理大型Golden Master文件?
A: 对于大型Golden Master文件:
- 使用压缩: 考虑使用
.gz压缩 - 分割文件: 将大型Golden Master分割为多个小文件
- 使用哈希: 比较文件哈希而不是内容
def test_large_golden_master():
"""测试大型Golden Master。"""
result = analyze_code_structure(file_path)
# 计算哈希而不是比较内容
import hashlib
result_hash = hashlib.md5(result.encode()).hexdigest()
with open(golden_master_path, 'rb') as f:
expected_hash = hashlib.md5(f.read()).hexdigest()
assert result_hash == expected_hash