回归测试指南

June 3, 2026 · View on GitHub

本文档为tree-sitter-analyzer项目提供全面的回归测试指南,帮助开发者理解和使用Golden Master方法进行回归测试。

📋 目录


回归测试概述

什么是回归测试?

回归测试是确保代码更改不会破坏现有功能的测试。它们验证:

  • API向后兼容性: 旧API仍然工作
  • 输出格式稳定性: 输出格式保持一致
  • 功能完整性: 现有功能继续正常工作

为什么需要回归测试?

  1. 防止破坏性变更: 捕获意外破坏现有功能的更改
  2. 确保向后兼容: 验证新版本与旧版本兼容
  3. 提高信心: 在发布前确保代码质量
  4. 自动化验证: 通过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最佳实践

  1. 版本控制: Golden Master文件应该提交到版本控制
  2. 明确命名: 使用描述性文件名(如python_sample_full.txt
  3. 文档化: 在测试文件中记录Golden Master的用途
  4. 定期审查: 定期检查Golden Master是否仍然有效
  5. 变更日志: 记录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: 验证测试

确保测试:

  1. 通过: 在当前代码上运行并通过
  2. 失败: 在有问题的代码上运行并失败
  3. 有意义: 失败消息清晰且有帮助

回归测试维护指南

何时更新Golden Master

更新Golden Master的情况:

  1. 有意更改: 功能性更改需要新输出格式
  2. Bug修复: 修复了导致错误输出的bug
  3. 新功能: 添加了新的输出字段
  4. 重构: 代码重构改变了输出顺序

不更新Golden Master的情况

  1. 代码清理: 不影响输出的代码改进
  2. 性能优化: 不影响输出的性能改进
  3. 文档更新: 仅文档更改

更新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

回归测试失败处理

当回归测试失败时:

  1. 分析失败原因:

    • 是有意更改吗?
    • 是意外bug吗?
    • 是格式问题吗?
  2. 如果是无意更改:

    • 修复导致失败的代码
    • 重新运行测试验证修复
  3. 如果是有意更改:

    • 更新Golden Master
    • 记录变更原因
    • 提交更新
  4. 如果是格式问题:

    • 检查代码格式化
    • 运行uv run black .
    • 重新运行测试

回归测试性能优化

如果回归测试运行缓慢:

  1. 使用pytest缓存:

    uv run pytest tests/ -m regression --cache-clear
    
  2. 并行运行:

    uv run pytest tests/ -m regression -n auto
    
  3. 选择性运行:

    # 只运行格式回归测试
    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文件:

  1. 使用压缩: 考虑使用.gz压缩
  2. 分割文件: 将大型Golden Master分割为多个小文件
  3. 使用哈希: 比较文件哈希而不是内容
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

参考资料