From 6152d49d18b7a371861edeb529c7b776a7da4fef Mon Sep 17 00:00:00 2001 From: Xiaoxia AI Date: Mon, 15 Jun 2026 16:20:11 +0800 Subject: [PATCH] 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 --- docs/API-SPEC.md | 375 ++++++++++++++++++++++++++++ docs/CODING-STANDARD.md | 449 ++++++++++++++++++++++++++++++++++ docs/TESTING-GUIDE.md | 529 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 1353 insertions(+) create mode 100644 docs/API-SPEC.md create mode 100644 docs/CODING-STANDARD.md create mode 100644 docs/TESTING-GUIDE.md diff --git a/docs/API-SPEC.md b/docs/API-SPEC.md new file mode 100644 index 000000000..2ed3442a1 --- /dev/null +++ b/docs/API-SPEC.md @@ -0,0 +1,375 @@ +# API 规范 + +本文档定义新 SaaS 项目的 REST API 设计规范。 + +--- + +## 1. 基本原则 + +- **RESTful** 设计 +- **JSON** 数据格式 +- **HTTP 状态码** 语义化 +- **版本化** API(未来) +- **文档化** (Swagger/ReDoc) + +--- + +## 2. URL 设计 + +### 2.1 资源命名 + +**使用复数名词**: +``` +✅ /api/projects +✅ /api/asset-libraries +✅ /api/assets +✅ /api/ingest-jobs + +❌ /api/project +❌ /api/assetLibrary +❌ /api/get_assets +``` + +**使用 kebab-case**: +``` +✅ /api/asset-libraries +✅ /api/ingest-jobs + +❌ /api/assetLibraries +❌ /api/ingest_jobs +``` + +### 2.2 路径层级 + +**浅层级**(推荐): +``` +✅ GET /api/projects?workspace_id=ws-1 +✅ GET /api/assets?library_id=lib-1 + +❌ GET /api/workspaces/ws-1/projects +❌ GET /api/projects/proj-1/libraries/lib-1/assets +``` + +**原因**: +- 避免深层嵌套 +- 查询参数更灵活 +- URL 更简洁 + +--- + +## 3. HTTP 方法 + +| 方法 | 用途 | 幂等性 | 安全性 | +|------|------|--------|--------| +| GET | 查询资源 | ✅ | ✅ | +| POST | 创建资源 | ❌ | ❌ | +| PUT | 完整更新 | ✅ | ❌ | +| PATCH | 部分更新 | ❌ | ❌ | +| DELETE | 删除资源 | ✅ | ❌ | + +### 3.1 示例 + +```http +# 查询项目列表 +GET /api/projects?workspace_id=ws-1 + +# 创建项目 +POST /api/projects +Content-Type: application/json +{ + "workspace_id": "ws-1", + "name": "新项目", + "description": "描述" +} + +# 更新项目 +PUT /api/projects/{project_id} +Content-Type: application/json +{ + "name": "更新后的名称", + "description": "更新后的描述" +} + +# 删除项目 +DELETE /api/projects/{project_id} +``` + +--- + +## 4. HTTP 状态码 + +### 4.1 成功响应 + +| 状态码 | 含义 | 使用场景 | +|--------|------|----------| +| 200 OK | 成功 | GET、PUT、PATCH | +| 201 Created | 创建成功 | POST | +| 204 No Content | 成功无内容 | DELETE | + +### 4.2 客户端错误 + +| 状态码 | 含义 | 使用场景 | +|--------|------|----------| +| 400 Bad Request | 请求参数错误 | 参数验证失败 | +| 401 Unauthorized | 未认证 | 缺少 token | +| 403 Forbidden | 无权限 | 权限不足 | +| 404 Not Found | 资源不存在 | 资源未找到 | +| 409 Conflict | 冲突 | 资源已存在 | +| 422 Unprocessable Entity | 业务逻辑错误 | 业务规则违反 | + +### 4.3 服务器错误 + +| 状态码 | 含义 | 使用场景 | +|--------|------|----------| +| 500 Internal Server Error | 服务器错误 | 未预期的异常 | +| 503 Service Unavailable | 服务不可用 | 维护中 | + +--- + +## 5. 请求格式 + +### 5.1 查询参数(GET) + +```http +GET /api/projects?workspace_id=ws-1&page=1&page_size=20 +``` + +**命名**:snake_case + +**分页参数**: +- `page` - 页码(从 1 开始) +- `page_size` - 每页数量(默认 20,最大 100) + +**过滤参数**: +- `workspace_id` - 按工作空间过滤 +- `kind` - 按类型过滤 +- `status` - 按状态过滤 + +**排序参数**: +- `sort_by` - 排序字段(如 `created_at`) +- `sort_order` - 排序方向(`asc` / `desc`) + +### 5.2 请求体(POST/PUT/PATCH) + +```json +{ + "workspace_id": "ws-1", + "name": "项目名称", + "description": "项目描述" +} +``` + +**命名**:snake_case + +--- + +## 6. 响应格式 + +### 6.1 单个资源 + +```json +{ + "id": "proj-123", + "workspace_id": "ws-1", + "name": "项目名称", + "description": "项目描述", + "created_at": "2026-06-15T08:00:00Z" +} +``` + +### 6.2 资源列表 + +```json +{ + "items": [ + { + "id": "proj-123", + "workspace_id": "ws-1", + "name": "项目 1" + }, + { + "id": "proj-456", + "workspace_id": "ws-1", + "name": "项目 2" + } + ], + "total": 42, + "page": 1, + "page_size": 20 +} +``` + +### 6.3 错误响应 + +```json +{ + "error": { + "code": "VALIDATION_ERROR", + "message": "项目名称不能为空", + "details": { + "field": "name", + "value": "" + } + } +} +``` + +**错误码**: +- `VALIDATION_ERROR` - 参数验证失败 +- `NOT_FOUND` - 资源不存在 +- `CONFLICT` - 资源冲突 +- `PERMISSION_DENIED` - 权限不足 +- `INTERNAL_ERROR` - 服务器错误 + +--- + +## 7. 认证与授权 + +### 7.1 认证(Phase 2) + +```http +GET /api/projects +Authorization: Bearer {token} +``` + +### 7.2 授权(Phase 3) + +```http +GET /api/projects/{project_id} +Authorization: Bearer {token} +X-Workspace-ID: ws-1 +``` + +--- + +## 8. 版本化(Phase 3) + +```http +GET /api/v1/projects +GET /api/v2/projects +``` + +--- + +## 9. 接口示例 + +### 9.1 项目管理 + +**创建项目**: +```http +POST /api/projects +Content-Type: application/json + +{ + "workspace_id": "ws-1", + "name": "新项目", + "description": "描述" +} + +Response 201: +{ + "id": "proj-123", + "workspace_id": "ws-1", + "name": "新项目", + "description": "描述", + "created_at": "2026-06-15T08:00:00Z" +} +``` + +**查询项目列表**: +```http +GET /api/projects?workspace_id=ws-1&page=1&page_size=20 + +Response 200: +{ + "items": [...], + "total": 42, + "page": 1, + "page_size": 20 +} +``` + +### 9.2 素材上传 + +**上传素材**: +```http +POST /api/upload +Content-Type: application/json + +{ + "workspace_id": "ws-1", + "project_id": "proj-1", + "library_id": "lib-1", + "filename": "video.mp4" +} + +Response 201: +{ + "storage_key": "uploads/abc123/video.mp4", + "ingest_job_id": "job-456" +} +``` + +### 9.3 任务查询 + +**查询入库任务**: +```http +GET /api/ingest-jobs/{job_id} + +Response 200: +{ + "id": "job-456", + "workspace_id": "ws-1", + "project_id": "proj-1", + "library_id": "lib-1", + "storage_key": "uploads/abc123/video.mp4", + "status": "completed", + "result_asset_id": "asset-789", + "created_at": "2026-06-15T08:00:00Z", + "updated_at": "2026-06-15T08:01:00Z" +} +``` + +--- + +## 10. 性能优化 + +### 10.1 分页 + +**强制分页**(避免返回大量数据): +```http +GET /api/assets?library_id=lib-1&page=1&page_size=20 +``` + +### 10.2 字段过滤(Phase 2) + +```http +GET /api/projects?fields=id,name +``` + +### 10.3 批量操作(Phase 2) + +```http +POST /api/assets/batch +Content-Type: application/json + +{ + "operations": [ + {"action": "delete", "id": "asset-1"}, + {"action": "delete", "id": "asset-2"} + ] +} +``` + +--- + +## 11. 文档化 + +使用 **FastAPI** 自动生成文档: +- Swagger UI: `http://localhost:8000/docs` +- ReDoc: `http://localhost:8000/redoc` + +--- + +**最后更新**: 2026-06-15 +**版本**: v1.0 diff --git a/docs/CODING-STANDARD.md b/docs/CODING-STANDARD.md new file mode 100644 index 000000000..cc9fa1a53 --- /dev/null +++ b/docs/CODING-STANDARD.md @@ -0,0 +1,449 @@ +# 编码规范 + +本文档定义新 SaaS 项目的 Python 编码规范。 + +--- + +## 1. 基础规范 + +遵循 [PEP 8](https://peps.python.org/pep-0008/),以下是重点和补充。 + +### 1.1 命名规范 + +**模块/包**:小写 + 下划线 +```python +# ✅ 正确 +from packages.domain import entities +from packages.adapters.in_memory import project_repository + +# ❌ 错误 +from packages.Domain import Entities +from packages.adapters.InMemory import ProjectRepository +``` + +**类**:PascalCase +```python +# ✅ 正确 +class Project: + pass + +class InMemoryProjectRepository: + pass + +# ❌ 错误 +class project: + pass + +class in_memory_project_repository: + pass +``` + +**函数/变量**:小写 + 下划线 +```python +# ✅ 正确 +def create_project(workspace_id: str, name: str) -> Project: + pass + +user_count = 10 + +# ❌ 错误 +def CreateProject(workspace_id: str, name: str) -> Project: + pass + +UserCount = 10 +``` + +**常量**:大写 + 下划线 +```python +# ✅ 正确 +MAX_PROJECT_NAME_LENGTH = 100 +DEFAULT_PAGE_SIZE = 20 + +# ❌ 错误 +maxProjectNameLength = 100 +default_page_size = 20 +``` + +**私有属性/方法**:前缀 `_` +```python +class Project: + def __init__(self): + self._internal_state = {} + + def _validate(self): + pass +``` + +--- + +## 2. Type Hints + +**强制使用** type hints,提升代码可读性和 IDE 支持。 + +```python +# ✅ 正确 +def create_project(workspace_id: str, name: str, description: str = "") -> Project: + pass + +def list_projects(workspace_id: str) -> list[Project]: + pass + +def get_project(project_id: str) -> Project | None: + pass + +# ❌ 错误 +def create_project(workspace_id, name, description=""): + pass +``` + +**复杂类型**: +```python +from typing import Protocol, Any + +# Dict/List +def update_metadata(metadata: dict[str, Any]) -> None: + pass + +# Optional (Python 3.10+ 用 | None) +def get_user(user_id: str) -> User | None: + pass + +# Protocol +class Repository(Protocol): + def get(self, id: str) -> Entity | None: + pass +``` + +--- + +## 3. Dataclass + +**优先使用** `dataclass` 定义实体和值对象。 + +```python +from dataclasses import dataclass, field +from datetime import datetime, timezone + +# ✅ 正确 +@dataclass(slots=True) +class Project: + id: str + workspace_id: str + name: str + description: str = "" + created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) + +# ❌ 错误(不用 dataclass) +class Project: + def __init__(self, id: str, workspace_id: str, name: str, description: str = ""): + self.id = id + self.workspace_id = workspace_id + self.name = name + self.description = description +``` + +**为什么用 `slots=True`?** +- 节省内存 +- 防止意外添加属性 +- 提升性能 + +--- + +## 4. 注释与文档 + +### 4.1 模块/类/函数注释 + +**使用中文注释**。 + +```python +def create_project(workspace_id: str, name: str, description: str = "") -> Project: + """ + 创建项目。 + + Args: + workspace_id: 工作空间 ID + name: 项目名称(不能为空) + description: 项目描述(可选) + + Returns: + 创建的项目实体 + + Raises: + ValueError: 项目名称为空时 + """ + pass +``` + +### 4.2 复杂逻辑注释 + +```python +# 正确做法:复杂逻辑加注释 +def calculate_priority(job: IngestJob) -> int: + # 优先级规则: + # 1. FAILED 状态最高(需要重试) + # 2. PENDING 状态次高(等待处理) + # 3. PROCESSING 状态最低(正在处理) + if job.status == IngestJobStatus.FAILED: + return 100 + elif job.status == IngestJobStatus.PENDING: + return 50 + else: + return 10 +``` + +--- + +## 5. 异常处理 + +### 5.1 使用具体异常 + +```python +# ✅ 正确 +def get_project(project_id: str) -> Project: + if not project_id: + raise ValueError("项目 ID 不能为空") + + project = repository.get(project_id) + if project is None: + raise KeyError(f"项目 {project_id} 不存在") + + return project + +# ❌ 错误 +def get_project(project_id: str) -> Project: + if not project_id: + raise Exception("错误") # 太宽泛 +``` + +### 5.2 自定义异常 + +```python +class ProjectNotFoundError(Exception): + """项目不存在异常。""" + pass + +class ProjectNameTooLongError(ValueError): + """项目名称过长异常。""" + pass +``` + +--- + +## 6. Clean Architecture 约束 + +### 6.1 依赖方向 + +``` +Apps (api/worker/web) + ↓ +Application (use cases) + ↓ +Ports (interfaces) ← Adapters (implementations) + ↓ +Domain (entities/rules) +``` + +**Domain 层**: +- ❌ 不能依赖任何外层 +- ❌ 不能依赖 SQLAlchemy、FastAPI、Celery +- ✅ 只能依赖 Python 标准库 + +```python +# ✅ 正确(Domain 层) +from dataclasses import dataclass +from datetime import datetime +from uuid import uuid4 + +@dataclass(slots=True) +class Project: + id: str + name: str + +# ❌ 错误(Domain 层) +from sqlalchemy import Column, String # ❌ 不能依赖 SQLAlchemy +from fastapi import HTTPException # ❌ 不能依赖 FastAPI + +@dataclass(slots=True) +class Project: + id: str + name: str +``` + +**Application 层**: +- ✅ 可以依赖 Domain + Ports +- ❌ 不能依赖 Adapters + +**Adapters 层**: +- ✅ 可以依赖 Domain + Ports +- ✅ 可以使用外部库(SQLAlchemy、Redis 等) + +--- + +## 7. 测试 + +### 7.1 测试文件命名 + +``` +tests/ +├── integration/ +│ ├── test_projects.py +│ ├── test_ingest_pipeline.py +│ └── test_classification_pipeline.py +└── unit/ + ├── test_project_entity.py + └── test_asset_validation.py +``` + +### 7.2 测试函数命名 + +```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 test1(): + pass + +def test_project(): + pass +``` + +### 7.3 测试结构(AAA 模式) + +```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 +``` + +--- + +## 8. 代码格式化 + +### 8.1 行长度 + +- 最大 120 字符 +- 优先 88 字符(Black 默认) + +### 8.2 导入顺序 + +```python +# 1. 标准库 +import os +from datetime import datetime +from typing import Protocol + +# 2. 第三方库 +from fastapi import FastAPI +from sqlalchemy import Column + +# 3. 本地模块 +from packages.domain import Project +from packages.application import CreateProjectUseCase +``` + +### 8.3 空行 + +```python +# 类之间:2 行 +class User: + pass + + +class Workspace: + pass + + +# 函数之间:1 行 +def create_user(): + pass + +def list_users(): + pass +``` + +--- + +## 9. 安全规范 + +### 9.1 禁止硬编码敏感信息 + +```python +# ❌ 错误 +DATABASE_URL = "postgresql://admin:password123@localhost/db" +API_KEY = "sk-1234567890abcdef" + +# ✅ 正确 +import os +DATABASE_URL = os.getenv("DATABASE_URL") +API_KEY = os.getenv("API_KEY") +``` + +### 9.2 输入验证 + +```python +# ✅ 正确 +def create_project(name: str) -> Project: + clean_name = name.strip() + if not clean_name: + raise ValueError("项目名称不能为空") + if len(clean_name) > 100: + raise ValueError("项目名称不能超过 100 字符") + + return Project(id=uuid4().hex, name=clean_name) +``` + +--- + +## 10. 性能规范 + +### 10.1 避免 N+1 查询 + +```python +# ❌ 错误 +projects = repository.list_by_workspace("ws-1") +for project in projects: + assets = asset_repository.list_by_project(project.id) # N+1 + +# ✅ 正确 +projects = repository.list_by_workspace("ws-1") +project_ids = [p.id for p in projects] +assets = asset_repository.list_by_projects(project_ids) # 一次查询 +``` + +### 10.2 使用生成器 + +```python +# ✅ 正确(大数据集) +def list_all_assets() -> Generator[Asset, None, None]: + for asset in repository.stream(): + yield asset + +# ❌ 错误(加载全部到内存) +def list_all_assets() -> list[Asset]: + return repository.list_all() # 可能 OOM +``` + +--- + +**最后更新**: 2026-06-15 +**版本**: v1.0 diff --git a/docs/TESTING-GUIDE.md b/docs/TESTING-GUIDE.md new file mode 100644 index 000000000..0d3428235 --- /dev/null +++ b/docs/TESTING-GUIDE.md @@ -0,0 +1,529 @@ +# 测试指南 + +本文档定义新 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