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
530 lines
11 KiB
Markdown
530 lines
11 KiB
Markdown
# 测试指南
|
||
|
||
本文档定义新 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
|
||
|
||
```bash
|
||
# 运行所有测试
|
||
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}`
|
||
|
||
```python
|
||
# ✅ 正确
|
||
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**
|
||
|
||
```python
|
||
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 测试完整业务流程
|
||
|
||
```python
|
||
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 实现
|
||
|
||
```python
|
||
from packages.adapters.in_memory import (
|
||
InMemoryProjectRepository,
|
||
InMemoryAssetRepository,
|
||
InMemoryIngestJobRepository,
|
||
)
|
||
|
||
def test_something():
|
||
# 使用 in-memory 实现,快速且无外部依赖
|
||
repository = InMemoryProjectRepository()
|
||
# ...
|
||
```
|
||
|
||
---
|
||
|
||
## 7. 测试场景覆盖
|
||
|
||
### 7.1 正常场景(Happy Path)
|
||
|
||
```python
|
||
def test_create_project_with_valid_data():
|
||
"""测试创建项目(正常场景)。"""
|
||
pass
|
||
|
||
def test_list_projects_returns_all_projects():
|
||
"""测试查询项目列表(正常场景)。"""
|
||
pass
|
||
```
|
||
|
||
### 7.2 边界条件(Boundary Cases)
|
||
|
||
```python
|
||
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)
|
||
|
||
```python
|
||
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)
|
||
|
||
```python
|
||
def test_concurrent_create_same_project():
|
||
"""测试并发创建相同项目(应处理冲突)。"""
|
||
# Phase 2 补充
|
||
pass
|
||
```
|
||
|
||
### 7.5 回归场景(Regression)
|
||
|
||
```python
|
||
def test_regression_ingest_job_status_not_reset():
|
||
"""回归测试:验证 IngestJob 状态不会被意外重置(bug #123)。"""
|
||
pass
|
||
```
|
||
|
||
---
|
||
|
||
## 8. Fixtures
|
||
|
||
### 8.1 共享 Fixtures
|
||
|
||
在 `conftest.py` 中定义:
|
||
|
||
```python
|
||
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="这是一个测试项目"
|
||
)
|
||
```
|
||
|
||
使用:
|
||
|
||
```python
|
||
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)
|
||
|
||
```python
|
||
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
|
||
```
|
||
|
||
运行特定标记的测试:
|
||
|
||
```bash
|
||
# 只运行集成测试
|
||
pytest -m integration
|
||
|
||
# 跳过慢速测试
|
||
pytest -m "not slow"
|
||
```
|
||
|
||
---
|
||
|
||
## 10. 断言(Assertions)
|
||
|
||
### 10.1 基本断言
|
||
|
||
```python
|
||
# 相等
|
||
assert result == expected
|
||
|
||
# 不相等
|
||
assert result != unexpected
|
||
|
||
# 包含
|
||
assert item in collection
|
||
assert key in dictionary
|
||
|
||
# 真值
|
||
assert condition
|
||
assert not condition
|
||
```
|
||
|
||
### 10.2 异常断言
|
||
|
||
```python
|
||
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 近似断言
|
||
|
||
```python
|
||
import pytest
|
||
|
||
# 浮点数近似相等
|
||
assert result == pytest.approx(0.85, rel=1e-2)
|
||
```
|
||
|
||
---
|
||
|
||
## 11. 测试数据
|
||
|
||
### 11.1 避免硬编码 ID
|
||
|
||
```python
|
||
# ❌ 错误
|
||
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 使用有意义的测试数据
|
||
|
||
```python
|
||
# ❌ 错误
|
||
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 查看覆盖率
|
||
|
||
```bash
|
||
# 生成 HTML 报告
|
||
pytest --cov=packages --cov=apps --cov-report=html
|
||
|
||
# 打开报告
|
||
open htmlcov/index.html # macOS/Linux
|
||
start htmlcov/index.html # Windows
|
||
```
|
||
|
||
---
|
||
|
||
## 13. 测试最佳实践
|
||
|
||
### 13.1 每个测试独立
|
||
|
||
```python
|
||
# ✅ 正确 - 每个测试独立
|
||
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 测试一件事
|
||
|
||
```python
|
||
# ❌ 错误 - 测试多件事
|
||
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 避免测试实现细节
|
||
|
||
```python
|
||
# ❌ 错误 - 测试实现细节
|
||
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)
|
||
|
||
```yaml
|
||
# .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
|