Files
agent-aide/aide-program/docs/07-测试规范.md
2025-12-13 04:37:41 +08:00

15 KiB
Raw Blame History

测试规范

一、测试概述

1.1 测试目标

确保 aide 程序的所有功能正确、稳定、可靠,满足设计要求。

1.2 测试范围

  • 单元测试:核心模块和函数
  • 集成测试:命令完整流程
  • 跨平台测试Linux、macOS、Windows
  • 性能测试:执行时间和资源占用

1.3 测试工具

  • pytest:测试框架
  • pytest-cov:代码覆盖率
  • pytest-mock:模拟和打桩
  • pytest-timeout:超时控制

二、测试结构

2.1 目录结构

tests/
├── __init__.py
├── conftest.py              # pytest 配置和 fixtures
├── unit/                    # 单元测试
│   ├── __init__.py
│   ├── test_output.py       # 输出模块测试
│   ├── test_config.py       # 配置模块测试
│   └── test_validators.py  # 验证函数测试
├── integration/             # 集成测试
│   ├── __init__.py
│   ├── test_init.py         # aide init 测试
│   ├── test_env.py          # aide env 测试
│   └── test_config_cmd.py   # aide config 测试
└── fixtures/                # 测试数据
    ├── sample_config.toml
    └── sample_gitignore

2.2 命名规范

  • 测试文件:test_<module>.py
  • 测试类:Test<Feature>
  • 测试函数:test_<scenario>

示例

# test_config.py
class TestConfig:
    def test_load_success(self):
        """测试成功加载配置"""
        pass

    def test_load_file_not_found(self):
        """测试配置文件不存在"""
        pass

三、单元测试

3.1 core/output.py 测试

# tests/unit/test_output.py
import pytest
from core.output import ok, warn, err, info

def test_ok_simple(capsys):
    """测试简单成功输出"""
    ok("操作成功")

    captured = capsys.readouterr()
    assert "✓ 操作成功" in captured.out

def test_ok_with_details(capsys):
    """测试带详细信息的成功输出"""
    ok("操作成功", ["详细信息1", "详细信息2"])

    captured = capsys.readouterr()
    assert "✓ 操作成功" in captured.out
    assert "  详细信息1" in captured.out
    assert "  详细信息2" in captured.out

def test_warn(capsys):
    """测试警告输出"""
    warn("这是警告")

    captured = capsys.readouterr()
    assert "⚠ 这是警告" in captured.out

def test_err(capsys):
    """测试错误输出"""
    err("这是错误", ["建议: 检查配置"])

    captured = capsys.readouterr()
    assert "✗ 这是错误" in captured.out
    assert "  建议: 检查配置" in captured.out

def test_info(capsys):
    """测试信息输出"""
    info("正在处理...")

    captured = capsys.readouterr()
    assert "→ 正在处理..." in captured.out

3.2 core/config.py 测试

# tests/unit/test_config.py
import pytest
from pathlib import Path
from core.config import Config, get_config_value, set_config_value

def test_get_config_value():
    """测试获取配置值"""
    config = {
        "task": {
            "source": "task-now.md"
        }
    }

    assert get_config_value(config, "task.source") == "task-now.md"
    assert get_config_value(config, "task.spec", "default") == "default"

def test_set_config_value():
    """测试设置配置值"""
    config = {"task": {}}

    set_config_value(config, "task.source", "new-task.md")
    assert config["task"]["source"] == "new-task.md"

def test_config_load(tmp_path):
    """测试加载配置"""
    # 创建测试配置文件
    config_dir = tmp_path / ".aide"
    config_dir.mkdir()
    config_path = config_dir / "config.toml"
    config_path.write_text('[task]\nsource = "test.md"\n')

    # 加载配置
    config = Config(tmp_path)
    config.load()

    assert config.get("task.source") == "test.md"

def test_config_load_not_found(tmp_path):
    """测试配置文件不存在"""
    config = Config(tmp_path)

    with pytest.raises(FileNotFoundError):
        config.load()

def test_config_save(tmp_path):
    """测试保存配置"""
    # 创建配置
    config_dir = tmp_path / ".aide"
    config_dir.mkdir()

    config = Config(tmp_path)
    config._config = {"task": {"source": "test.md"}}
    config.save()

    # 验证文件已创建
    config_path = tmp_path / ".aide" / "config.toml"
    assert config_path.exists()

    # 验证内容
    content = config_path.read_text()
    assert "task" in content
    assert "test.md" in content

3.3 utils/validators.py 测试

# tests/unit/test_validators.py
import pytest
from utils.validators import is_valid_version_spec, validate_config

def test_is_valid_version_spec():
    """测试版本规格验证"""
    assert is_valid_version_spec(">=3.10") == True
    assert is_valid_version_spec("3.12") == True
    assert is_valid_version_spec(">=3.10,<4.0") == True
    assert is_valid_version_spec("invalid") == False

def test_validate_config():
    """测试配置验证"""
    # 有效配置
    valid_config = {
        "task": {"source": "task.md", "spec": "spec.md"},
        "env": {"python": {"version": ">=3.10"}},
        "output": {"language": "zh-CN"}
    }
    errors = validate_config(valid_config)
    assert len(errors) == 0

    # 缺少必需项
    invalid_config = {"task": {}}
    errors = validate_config(invalid_config)
    assert len(errors) > 0
    assert any("task.source" in e for e in errors)

四、集成测试

4.1 aide init 测试

# tests/integration/test_init.py
import pytest
from pathlib import Path
from commands.init import cmd_init

def test_init_success(tmp_path, monkeypatch):
    """测试初始化成功"""
    monkeypatch.chdir(tmp_path)

    result = cmd_init([])

    assert result == 0
    assert (tmp_path / ".aide").exists()
    assert (tmp_path / ".aide" / "config.toml").exists()
    assert (tmp_path / ".gitignore").exists()

def test_init_idempotent(tmp_path, monkeypatch):
    """测试幂等性"""
    monkeypatch.chdir(tmp_path)

    # 第一次初始化
    result1 = cmd_init([])
    assert result1 == 0

    # 读取配置内容
    config_path = tmp_path / ".aide" / "config.toml"
    content1 = config_path.read_text()

    # 第二次初始化
    result2 = cmd_init([])
    assert result2 == 0

    # 验证配置未改变
    content2 = config_path.read_text()
    assert content1 == content2

def test_init_permission_error(tmp_path, monkeypatch):
    """测试权限错误"""
    monkeypatch.chdir(tmp_path)

    # 设置只读权限
    tmp_path.chmod(0o444)

    result = cmd_init([])

    # 恢复权限
    tmp_path.chmod(0o755)

    assert result != 0

4.2 aide env 测试

# tests/integration/test_env.py
import pytest
from commands.env import cmd_env, env_ensure_runtime, env_ensure_project

def test_env_ensure_runtime_success():
    """测试运行时环境检测成功"""
    result = env_ensure_runtime()
    assert result == 0

def test_env_ensure_project_success(tmp_path, monkeypatch):
    """测试项目环境检测成功"""
    monkeypatch.chdir(tmp_path)

    # 初始化项目
    from commands.init import cmd_init
    cmd_init([])

    # 检测环境
    result = env_ensure_project()
    assert result == 0

def test_env_ensure_project_no_config(tmp_path, monkeypatch):
    """测试配置文件不存在"""
    monkeypatch.chdir(tmp_path)

    result = env_ensure_project()
    assert result == 4

def test_env_ensure_create_venv(tmp_path, monkeypatch):
    """测试自动创建虚拟环境"""
    monkeypatch.chdir(tmp_path)

    # 初始化项目
    from commands.init import cmd_init
    cmd_init([])

    # 检测环境(应该自动创建虚拟环境)
    result = env_ensure_project()
    assert result == 0

    # 验证虚拟环境已创建
    assert (tmp_path / ".venv").exists()

4.3 aide config 测试

# tests/integration/test_config_cmd.py
import pytest
from commands.config_cmd import cmd_config, config_get, config_set

def test_config_get_success(tmp_path, monkeypatch, capsys):
    """测试获取配置成功"""
    monkeypatch.chdir(tmp_path)

    # 初始化
    from commands.init import cmd_init
    cmd_init([])

    # 获取配置
    result = config_get("task.source")
    assert result == 0

    captured = capsys.readouterr()
    assert "task.source" in captured.out
    assert "task-now.md" in captured.out

def test_config_set_success(tmp_path, monkeypatch):
    """测试设置配置成功"""
    monkeypatch.chdir(tmp_path)

    # 初始化
    from commands.init import cmd_init
    cmd_init([])

    # 设置配置
    result = config_set("task.source", "new-task.md")
    assert result == 0

    # 验证配置已更新
    result = config_get("task.source")
    assert result == 0

def test_config_cmd_no_args(capsys):
    """测试无参数"""
    result = cmd_config([])
    assert result == 2

    captured = capsys.readouterr()
    assert "缺少子命令" in captured.out

五、跨平台测试

5.1 路径处理测试

def test_path_handling_cross_platform(tmp_path):
    """测试跨平台路径处理"""
    from pathlib import Path

    # 使用 pathlib 确保跨平台兼容
    aide_dir = tmp_path / ".aide"
    config_path = aide_dir / "config.toml"

    aide_dir.mkdir()
    config_path.write_text("test")

    assert config_path.exists()
    assert config_path.is_file()

5.2 命令执行测试

@pytest.mark.skipif(sys.platform == "win32", reason="Unix only")
def test_unix_specific():
    """Unix 特定测试"""
    pass

@pytest.mark.skipif(sys.platform != "win32", reason="Windows only")
def test_windows_specific():
    """Windows 特定测试"""
    pass

六、性能测试

6.1 执行时间测试

import time

def test_init_performance(tmp_path, monkeypatch):
    """测试初始化性能"""
    monkeypatch.chdir(tmp_path)

    start = time.time()
    result = cmd_init([])
    elapsed = time.time() - start

    assert result == 0
    assert elapsed < 0.2  # 应该在 200ms 内完成

def test_config_get_performance(tmp_path, monkeypatch):
    """测试配置读取性能"""
    monkeypatch.chdir(tmp_path)

    # 初始化
    cmd_init([])

    # 测试性能
    start = time.time()
    result = config_get("task.source")
    elapsed = time.time() - start

    assert result == 0
    assert elapsed < 0.1  # 应该在 100ms 内完成

6.2 内存占用测试

import psutil
import os

def test_memory_usage():
    """测试内存占用"""
    process = psutil.Process(os.getpid())
    mem_before = process.memory_info().rss / 1024 / 1024  # MB

    # 执行操作
    cmd_init([])

    mem_after = process.memory_info().rss / 1024 / 1024  # MB
    mem_used = mem_after - mem_before

    assert mem_used < 50  # 应该小于 50MB

七、测试覆盖率

7.1 覆盖率要求

  • 核心模块:≥ 80%
  • 命令模块:≥ 80%
  • 工具模块:≥ 70%
  • 整体:≥ 75%

7.2 运行覆盖率测试

# 运行测试并生成覆盖率报告
pytest --cov=src --cov-report=html --cov-report=term

# 查看覆盖率报告
open htmlcov/index.html

7.3 覆盖率配置

# .coveragerc
[run]
source = src
omit =
    */tests/*
    */venv/*
    */__pycache__/*

[report]
exclude_lines =
    pragma: no cover
    def __repr__
    raise AssertionError
    raise NotImplementedError
    if __name__ == .__main__.:

八、测试 Fixtures

8.1 conftest.py

# tests/conftest.py
import pytest
from pathlib import Path
import shutil

@pytest.fixture
def temp_project(tmp_path):
    """创建临时项目目录"""
    project_dir = tmp_path / "test_project"
    project_dir.mkdir()
    return project_dir

@pytest.fixture
def initialized_project(temp_project, monkeypatch):
    """创建已初始化的项目"""
    monkeypatch.chdir(temp_project)
    from commands.init import cmd_init
    cmd_init([])
    return temp_project

@pytest.fixture
def sample_config():
    """示例配置"""
    return {
        "task": {
            "source": "task-now.md",
            "spec": "task-spec.md"
        },
        "env": {
            "python": {
                "version": ">=3.10",
                "venv": ".venv"
            }
        }
    }

九、持续集成

9.1 GitHub Actions 配置

# .github/workflows/test.yml
name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        python-version: ['3.10', '3.11', '3.12']

    steps:
    - uses: actions/checkout@v3

    - name: Set up Python
      uses: actions/setup-python@v4
      with:
        python-version: ${{ matrix.python-version }}

    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install pytest pytest-cov pytest-mock
        pip install tomli tomli-w

    - name: Run tests
      run: |
        pytest --cov=src --cov-report=xml

    - name: Upload coverage
      uses: codecov/codecov-action@v3
      with:
        file: ./coverage.xml

十、测试最佳实践

10.1 测试原则

  1. 独立性:每个测试独立运行,不依赖其他测试
  2. 可重复性:多次运行结果一致
  3. 清晰性:测试意图明确,易于理解
  4. 快速性:测试执行快速,及时反馈

10.2 测试模式

AAA 模式Arrange-Act-Assert

def test_example():
    # Arrange准备测试数据
    config = {"task": {"source": "test.md"}}

    # Act执行操作
    result = get_config_value(config, "task.source")

    # Assert验证结果
    assert result == "test.md"

10.3 Mock 使用

from unittest.mock import Mock, patch

def test_with_mock(monkeypatch):
    """使用 mock 测试"""
    # Mock 函数
    mock_func = Mock(return_value=True)
    monkeypatch.setattr("module.function", mock_func)

    # 执行测试
    result = some_function()

    # 验证 mock 被调用
    mock_func.assert_called_once()

十一、测试检查清单

11.1 单元测试

  • core/output.py 所有函数
  • core/config.py 所有函数
  • utils/validators.py 所有函数
  • 边界条件测试
  • 错误路径测试

11.2 集成测试

  • aide init 完整流程
  • aide env ensure --runtime
  • aide env ensure
  • aide config get
  • aide config set
  • 命令组合测试

11.3 跨平台测试

  • Linux 测试通过
  • macOS 测试通过
  • Windows 测试通过
  • 路径处理正确

11.4 性能测试

  • 执行时间符合要求
  • 内存占用符合要求
  • 无内存泄漏

11.5 覆盖率

  • 核心模块 ≥ 80%
  • 命令模块 ≥ 80%
  • 整体 ≥ 75%

十二、总结

12.1 核心要点

  1. 完整的测试覆盖(单元、集成、跨平台)
  2. 明确的覆盖率要求
  3. 持续集成自动化
  4. 遵循测试最佳实践

12.2 运行测试

# 运行所有测试
pytest

# 运行特定测试
pytest tests/unit/test_config.py

# 运行带覆盖率的测试
pytest --cov=src

# 运行性能测试
pytest -m performance

# 运行跨平台测试
pytest -m cross_platform

版本v1.0 更新日期2025-12-13