Files
xiaoxia-saas/docs/TESTING-GUIDE.md
T
Xiaoxia AI 6152d49d18 docs: add coding/API/testing standards
- 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
2026-06-15 16:20:11 +08:00

530 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 测试指南
本文档定义新 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