6152d49d18
- CODING-STANDARD.md: PEP 8, type hints, Clean Architecture constraints, security - API-SPEC.md: RESTful design, HTTP methods, status codes, request/response format - TESTING-GUIDE.md: test strategy, AAA pattern, fixtures, coverage targets - complete examples included
11 KiB
11 KiB
测试指南
本文档定义新 SaaS 项目的测试策略、测试规范和最佳实践。
1. 测试策略
1.1 测试金字塔
E2E Tests (少量)
/ \
Integration Tests (中量)
/ \
Unit Tests (大量,按需)
当前阶段(Phase 1):
- 集成测试优先 - 覆盖完整业务流程
- 单元测试辅助 - 覆盖复杂逻辑
- E2E 测试占位 - Phase 2-3 补充
原因:
- 集成测试验证架构正确性
- 集成测试覆盖核心业务流程
- In-Memory 实现使得集成测试成本低
2. 测试工具
测试框架: pytest
测试环境: in-memory 或 SQLite
覆盖率: pytest-cov
# 运行所有测试
pytest tests/integration/ -v
# 运行指定测试
pytest tests/integration/test_projects.py -v
# 运行带覆盖率
pytest --cov=packages --cov=apps --cov-report=html
# 运行快速测试(跳过慢速测试)
pytest -m "not slow"
3. 测试结构
tests/
├── conftest.py # 共享 fixtures
├── integration/ # 集成测试
│ ├── test_projects.py
│ ├── test_ingest_pipeline.py
│ ├── test_classification_pipeline.py
│ └── test_upload_pipeline.py
├── unit/ # 单元测试
│ ├── test_project_entity.py
│ └── test_asset_validation.py
└── e2e/ # 端到端测试(占位)
└── README.md
4. 测试命名
4.1 测试文件
test_{module_name}.py
4.2 测试函数
推荐格式:test_{what}_{condition}
# ✅ 正确
def test_create_project_with_valid_name():
pass
def test_create_project_with_empty_name_should_fail():
pass
def test_list_projects_by_workspace():
pass
def test_ingest_asset_updates_job_status_to_completed():
pass
# ❌ 错误
def test1():
pass
def test_project():
pass
def test_stuff():
pass
5. 测试结构(AAA 模式)
Arrange - Act - Assert
def test_create_project():
# Arrange(准备)
workspace_id = "ws-1"
name = "测试项目"
repository = InMemoryProjectRepository()
use_case = CreateProjectUseCase(repository)
# Act(执行)
project = use_case.execute(
CreateProjectCommand(workspace_id=workspace_id, name=name)
)
# Assert(断言)
assert project.name == name
assert project.workspace_id == workspace_id
assert project.id != ""
6. 集成测试
6.1 测试完整业务流程
def test_upload_to_asset_full_pipeline():
"""测试完整上传链路:上传 → storage → 入库任务 → worker → asset 创建。"""
# Arrange
job_repo = InMemoryIngestJobRepository()
asset_repo = InMemoryAssetRepository()
# Act - 提交入库任务
use_case = SubmitIngestJobUseCase(job_repo)
job = use_case.execute(
SubmitIngestJobCommand(
workspace_id="ws-1",
project_id="proj-1",
library_id="lib-1",
storage_key="uploads/abc123/video.mp4",
)
)
# Act - 模拟 worker 处理
result = simulate_ingest_asset(job.id, job_repo, asset_repo)
# Assert - 验证任务完成
assert result["status"] == "completed"
updated_job = job_repo.get(job.id)
assert updated_job.status == IngestJobStatus.COMPLETED
# Assert - 验证 asset 创建
assets = asset_repo.list_by_library("lib-1")
assert len(assets) == 1
assert assets[0].storage_key == "uploads/abc123/video.mp4"
6.2 使用 In-Memory 实现
from packages.adapters.in_memory import (
InMemoryProjectRepository,
InMemoryAssetRepository,
InMemoryIngestJobRepository,
)
def test_something():
# 使用 in-memory 实现,快速且无外部依赖
repository = InMemoryProjectRepository()
# ...
7. 测试场景覆盖
7.1 正常场景(Happy Path)
def test_create_project_with_valid_data():
"""测试创建项目(正常场景)。"""
pass
def test_list_projects_returns_all_projects():
"""测试查询项目列表(正常场景)。"""
pass
7.2 边界条件(Boundary Cases)
def test_create_project_with_empty_name_should_fail():
"""测试创建项目(项目名为空应失败)。"""
with pytest.raises(ValueError, match="项目名称不能为空"):
Project.create(workspace_id="ws-1", name="")
def test_create_project_with_max_length_name():
"""测试创建项目(项目名最大长度)。"""
long_name = "a" * 100
project = Project.create(workspace_id="ws-1", name=long_name)
assert len(project.name) == 100
def test_create_project_with_too_long_name_should_fail():
"""测试创建项目(项目名超长应失败)。"""
too_long_name = "a" * 101
with pytest.raises(ValueError, match="项目名称不能超过"):
Project.create(workspace_id="ws-1", name=too_long_name)
7.3 异常场景(Error Cases)
def test_get_nonexistent_project_returns_none():
"""测试查询不存在的项目(应返回 None)。"""
repository = InMemoryProjectRepository()
project = repository.get("nonexistent-id")
assert project is None
def test_update_nonexistent_project_should_fail():
"""测试更新不存在的项目(应失败)。"""
repository = InMemoryProjectRepository()
with pytest.raises(ValueError, match="项目.*不存在"):
repository.update(Project(id="nonexistent-id", ...))
7.4 并发场景(Concurrency)
def test_concurrent_create_same_project():
"""测试并发创建相同项目(应处理冲突)。"""
# Phase 2 补充
pass
7.5 回归场景(Regression)
def test_regression_ingest_job_status_not_reset():
"""回归测试:验证 IngestJob 状态不会被意外重置(bug #123)。"""
pass
8. Fixtures
8.1 共享 Fixtures
在 conftest.py 中定义:
import pytest
from packages.adapters.in_memory import InMemoryProjectRepository
@pytest.fixture
def project_repository():
"""项目仓储 fixture。"""
return InMemoryProjectRepository()
@pytest.fixture
def sample_project():
"""示例项目 fixture。"""
return Project.create(
workspace_id="ws-1",
name="测试项目",
description="这是一个测试项目"
)
使用:
def test_create_project(project_repository):
"""测试创建项目。"""
project = Project.create(workspace_id="ws-1", name="新项目")
saved = project_repository.create(project)
assert saved.id == project.id
def test_list_projects(project_repository, sample_project):
"""测试查询项目列表。"""
project_repository.create(sample_project)
projects = project_repository.list_by_workspace("ws-1")
assert len(projects) == 1
9. 测试标记(Markers)
import pytest
@pytest.mark.slow
def test_large_dataset():
"""慢速测试(大数据集)。"""
pass
@pytest.mark.integration
def test_full_pipeline():
"""集成测试。"""
pass
@pytest.mark.unit
def test_entity_validation():
"""单元测试。"""
pass
运行特定标记的测试:
# 只运行集成测试
pytest -m integration
# 跳过慢速测试
pytest -m "not slow"
10. 断言(Assertions)
10.1 基本断言
# 相等
assert result == expected
# 不相等
assert result != unexpected
# 包含
assert item in collection
assert key in dictionary
# 真值
assert condition
assert not condition
10.2 异常断言
import pytest
# 验证抛出异常
with pytest.raises(ValueError):
Project.create(workspace_id="ws-1", name="")
# 验证异常消息
with pytest.raises(ValueError, match="项目名称不能为空"):
Project.create(workspace_id="ws-1", name="")
10.3 近似断言
import pytest
# 浮点数近似相等
assert result == pytest.approx(0.85, rel=1e-2)
11. 测试数据
11.1 避免硬编码 ID
# ❌ 错误
def test_create_project():
project = Project(id="proj-123", ...)
# ✅ 正确
def test_create_project():
project = Project.create(workspace_id="ws-1", name="测试项目")
assert project.id != "" # ID 由系统生成
11.2 使用有意义的测试数据
# ❌ 错误
def test_create_project():
project = Project.create(workspace_id="a", name="b")
# ✅ 正确
def test_create_project():
project = Project.create(
workspace_id="ws-test-001",
name="电商平台项目",
description="2026 年 Q2 电商平台重构项目"
)
12. 测试覆盖率
12.1 目标覆盖率
- Domain 层: 100%
- Application 层: 90%+
- Adapters 层: 80%+
- API 层: 70%+
12.2 查看覆盖率
# 生成 HTML 报告
pytest --cov=packages --cov=apps --cov-report=html
# 打开报告
open htmlcov/index.html # macOS/Linux
start htmlcov/index.html # Windows
13. 测试最佳实践
13.1 每个测试独立
# ✅ 正确 - 每个测试独立
def test_create_project():
repository = InMemoryProjectRepository()
project = Project.create(workspace_id="ws-1", name="项目 1")
repository.create(project)
def test_list_projects():
repository = InMemoryProjectRepository() # 新实例
project = Project.create(workspace_id="ws-1", name="项目 2")
repository.create(project)
projects = repository.list_by_workspace("ws-1")
assert len(projects) == 1
13.2 测试一件事
# ❌ 错误 - 测试多件事
def test_project_crud():
repository = InMemoryProjectRepository()
# 创建
project = Project.create(...)
repository.create(project)
# 查询
found = repository.get(project.id)
# 更新
found.name = "新名称"
repository.update(found)
# 删除
repository.delete(project.id)
# ✅ 正确 - 拆分成多个测试
def test_create_project():
pass
def test_get_project():
pass
def test_update_project():
pass
def test_delete_project():
pass
13.3 避免测试实现细节
# ❌ 错误 - 测试实现细节
def test_project_repository_uses_dict():
repository = InMemoryProjectRepository()
assert isinstance(repository._items, dict)
# ✅ 正确 - 测试行为
def test_project_repository_stores_project():
repository = InMemoryProjectRepository()
project = Project.create(workspace_id="ws-1", name="项目")
repository.create(project)
found = repository.get(project.id)
assert found is not None
assert found.name == "项目"
14. 持续集成(Phase 2)
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: '3.12'
- run: pip install -r requirements.txt
- run: pytest tests/ --cov=packages --cov=apps
最后更新: 2026-06-15
版本: v1.0