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
This commit is contained in:
Xiaoxia AI
2026-06-15 16:20:11 +08:00
parent 5ea028cafd
commit 6152d49d18
3 changed files with 1353 additions and 0 deletions
+375
View File
@@ -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
+449
View File
@@ -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
+529
View File
@@ -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