本文档说明 Claude How To 的测试基础设施。

概览

项目使用 GitHub Actions 在每次 push 和 pull request 时自动运行测试。测试覆盖:

在本地运行测试

前置条件

# 安装 uv(快速 Python 包管理器)
pip install uv

# 或者在 macOS 上使用 Homebrew
brew install uv

配置环境

# 克隆仓库
git clone YOUR_REPO
cd claude-howto

# 创建虚拟环境
uv venv

# 激活虚拟环境
source .venv/bin/activate  # macOS/Linux
# 或者
.venv\Scripts\activate     # Windows

# 安装开发依赖
uv pip install -r requirements-dev.txt

运行测试

# 运行所有单元测试
pytest scripts/tests/ -v

# 运行带覆盖率的测试
pytest scripts/tests/ -v --cov=scripts --cov-report=html

# 运行指定测试文件
pytest scripts/tests/test_build_epub.py -v

# 运行指定测试函数
pytest scripts/tests/test_build_epub.py::test_function_name -v

# 以 watch 模式运行测试(需要 pytest-watch)
ptw scripts/tests/

运行 lint

# 检查代码格式
ruff format --check scripts/

# 自动修复格式问题
ruff format scripts/

# 运行 lint
ruff check scripts/

# 自动修复 lint 问题
ruff check --fix scripts/

运行安全扫描

# 运行 Bandit 安全扫描
bandit -c pyproject.toml -r scripts/ --exclude scripts/tests/

# 生成 JSON 报告
bandit -c pyproject.toml -r scripts/ --exclude scripts/tests/ -f json -o bandit-report.json

运行类型检查

# 使用 mypy 检查类型
mypy scripts/ --ignore-missing-imports --no-implicit-optional

GitHub Actions 工作流

触发条件

作业

1. 单元测试(pytest)

结果:如果任何测试失败,工作流失败(关键)

2. 代码质量(Ruff)

结果:非阻塞(仅警告)

3. 安全扫描(Bandit)

结果:非阻塞(仅警告)

4. 类型检查(mypy)

结果:非阻塞(仅警告)

5. 构建 EPUB

结果:如果构建失败,工作流失败(关键)

6. 总结

编写测试

测试结构

测试应放在 scripts/tests/ 中,文件名形如 test_*.py

# scripts/tests/test_example.py
import pytest
from scripts.example_module import some_function

def test_basic_functionality():
    """测试 some_function 是否正常工作。"""
    result = some_function("input")
    assert result == "expected_output"

def test_error_handling():
    """测试 some_function 是否能优雅处理错误。"""
    with pytest.raises(ValueError):
        some_function("invalid_input")

@pytest.mark.asyncio
async def test_async_function():
    """测试异步函数。"""
    result = await async_function()
    assert result is not None

测试最佳实践

Fixtures

常用 fixture 定义在 scripts/tests/conftest.py

# 在测试中使用 fixture
def test_something(tmp_path):
    """tmp_path fixture 提供临时目录。"""
    test_file = tmp_path / "test.txt"
    test_file.write_text("content")
    assert test_file.read_text() == "content"

覆盖率报告

本地覆盖率

# 生成覆盖率报告
pytest scripts/tests/ --cov=scripts --cov-report=html

# 在浏览器中打开覆盖率报告
open htmlcov/index.html

覆盖率目标

Pre-commit Hooks

项目使用 pre-commit hooks 在每次提交前自动运行检查:

# 安装 pre-commit hooks
pre-commit install

# 手动运行 hooks
pre-commit run --all-files

# 跳过某次提交的 hooks(不推荐)
git commit --no-verify

.pre-commit-config.yaml 中配置的 hooks:

排障

本地测试通过,但 CI 失败

常见原因:

  1. Python 版本差异:CI 使用 3.10、3.11、3.12
  2. 依赖缺失:更新 requirements-dev.txt
  3. 平台差异:路径分隔符、环境变量
  4. 测试不稳定:依赖时序或执行顺序的测试

解决方案:

# 使用相同的 Python 版本测试
uv python install 3.10 3.11 3.12

# 使用干净环境测试
rm -rf .venv
uv venv
uv pip install -r requirements-dev.txt
pytest scripts/tests/

Bandit 报告误报

某些安全警告可能是误报。可在 pyproject.toml 中配置:

[tool.bandit]
exclude_dirs = ["scripts/tests"]
skips = ["B101"]  # 跳过 assert_used 警告

类型检查太严格

对特定文件放宽类型检查:

# 放在文件顶部
# type: ignore

# 或者针对特定行
some_dynamic_code()  # type: ignore

持续集成最佳实践

  1. 保持测试快速:每个测试最好在 1 秒内完成
  2. 不要测试外部 API:用 mock 替代外部服务
  3. 测试要隔离:每个测试都应独立
  4. 断言要清晰:写 assert x == 5,不要写 assert x
  5. 处理异步测试:使用 @pytest.mark.asyncio
  6. 生成报告:覆盖率、安全扫描、类型检查

资源

贡献测试

提交 PR 时:

  1. 为新功能编写测试
  2. 本地运行测试pytest scripts/tests/ -v
  3. 检查覆盖率pytest scripts/tests/ --cov=scripts
  4. 运行 lintruff check scripts/
  5. 安全扫描bandit -r scripts/ --exclude scripts/tests/
  6. 如果测试变化,更新文档

所有 PR 都必须包含测试!🧪


如果你对测试有问题或疑问,请在 GitHub 上创建 issue 或 discussion。