geo/docs/03-development/tdd-workflow.md

583 lines
18 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.

# GEO 平台 - TDD 流程规范
## 概述
本文档定义 GEO 平台的测试驱动开发TDD工作流程。所有功能开发必须遵循 TDD 原则:先写测试,再写实现,最后重构。
TDD 的核心价值:
- **设计先行**:通过测试用例明确需求和接口设计
- **快速反馈**:自动化测试提供即时的正确性验证
- **安全重构**:测试覆盖确保重构不会破坏功能
- **文档即代码**:测试用例本身就是最准确的接口文档
## RED-GREEN-REFACTOR 循环
### 基本循环
```
┌──────────────────────┐
│ 1. RED 编写失败测试 │
│ 写一个会失败的测试用例 │
└───────────┬──────────┘
┌──────────────────────┐
│ 2. GREEN 通过测试 │
│ 写最少代码让测试通过 │
└───────────┬──────────┘
┌──────────────────────┐
│ 3. REFACTOR 重构代码 │
│ 优化实现,保持测试通过 │
└───────────┬──────────┘
└──────▶ 下一个循环
```
### 各阶段要求
#### RED 阶段 - 编写失败测试
- **前提**:已完成需求分析和接口设计
- **动作**:编写一个测试用例,验证某个具体功能点
- **验证**:运行测试,确认测试失败(且失败原因符合预期)
- **原则**
- 一次只写一个测试
- 测试命名清晰表达被测行为
- 使用 Given-When-Then 结构组织测试代码
- 测试失败信息应清晰说明期望 vs 实际
**示例Python + pytest**
```python
# tests/test_citation_detector.py
class TestCitationDetector:
def test_should_detect_direct_brand_reference(self):
"""Given: AI 响应中包含品牌直接引用
When: 执行引用检测
Then: 返回引用类型为 direct_quote"""
# Given
response = "根据 ExampleBrand 的研究报告显示..."
brand = "ExampleBrand"
detector = CitationDetector()
# When
result = detector.detect(response, brand)
# Then
assert result.reference_type == ReferenceType.DIRECT_QUOTE
assert result.confidence >= 0.8
```
#### GREEN 阶段 - 通过测试
- **前提**:已有一个失败的测试
- **动作**:编写最少量的生产代码使测试通过
- **验证**:运行测试,确认测试通过
- **原则**
- 不择手段让测试通过(可以写丑陋的代码)
- 不要提前实现未测试的功能
- 如果测试仍然失败,调试直到通过
- 如果通过后发现测试有问题,返回 RED 阶段
**示例**
```python
# app/services/citation_detector.py
class CitationDetector:
def detect(self, response: str, brand: str) -> DetectionResult:
# 最小实现:直接返回预期结果
if brand in response:
return DetectionResult(
reference_type=ReferenceType.DIRECT_QUOTE,
confidence=0.9
)
return DetectionResult(
reference_type=ReferenceType.NO_REFERENCE,
confidence=1.0
)
```
#### REFACTOR 阶段 - 重构代码
- **前提**:所有测试通过
- **动作**:优化代码结构、消除重复、提升可读性
- **验证**:持续运行测试,确保测试始终通过
- **原则**
- 小步重构,每次改动后运行测试
- 遵循 SOLID 原则和设计模式
- 不引入新功能(新功能走新的 TDD 循环)
- 关注代码异味(重复、过长函数、魔法数字等)
**重构后示例**
```python
# app/services/citation_detector.py
class CitationDetector:
def detect(self, response: str, brand: str) -> DetectionResult:
if not response or not brand:
raise ValueError("Response and brand are required")
if self._is_direct_reference(response, brand):
return self._create_result(ReferenceType.DIRECT_QUOTE, 0.9)
if self._is_indirect_reference(response, brand):
return self._create_result(ReferenceType.INDIRECT_REFERENCE, 0.6)
return self._create_result(ReferenceType.NO_REFERENCE, 1.0)
def _is_direct_reference(self, response: str, brand: str) -> bool:
return brand in response
def _create_result(self, ref_type: ReferenceType, confidence: float) -> DetectionResult:
return DetectionResult(reference_type=ref_type, confidence=confidence)
```
## 测试层次
GEO 平台采用四层测试金字塔:
```
/│\
/ │ \ E2E 测试(端到端)
/ │ \ 覆盖关键用户旅程
/───┼───\ 占比5%
/ │ \
/─────┼─────\ 集成测试
/ │ \ 模块间交互验证
/───────┼───────\ 占比15%
/ │ \
/─────────┼─────────\ 单元测试
/ │ \ 单个函数/类验证
/───────────┼───────────\ 占比60%
/ │ \
/─────────────┼─────────────\ Agent 测试
/ │ \ Agent 行为验证
/───────────────┼───────────────\ 占比20%
───────────────────────────────────
```
### 1. 单元测试Unit Tests
| 属性 | 说明 |
|------|------|
| **目标** | 验证单个函数、类或方法的行为 |
| **范围** | 隔离测试Mock 所有外部依赖 |
| **速度** | 毫秒级,可频繁运行 |
| **工具** | Python: `pytest` + `unittest.mock`TypeScript: `Jest` |
| **位置** | `tests/unit/` 或与被测代码同目录 `__tests__/` |
| **命名** | `test_{被测单元}_{测试场景}.py` |
**单元测试原则**
- **FIRST 原则**Fast快速、Independent独立、Repeatable可重复、Self-validating自验证、Timely及时
- **AAA 模式**Arrange准备、Act执行、Assert断言
- **一个断言原则**:每个测试验证一个概念
- **Mock 外部依赖**数据库、HTTP 请求、文件系统全部 Mock
**示例**
```python
# tests/unit/test_citation_service.py
class TestCitationService:
@pytest.fixture
def mock_repo(self):
return Mock(spec=CitationRepository)
@pytest.fixture
def service(self, mock_repo):
return CitationService(repository=mock_repo)
def test_should_save_citation_with_confidence(self, service, mock_repo):
# Arrange
citation = CitationCreate(brand="TestBrand", confidence=0.85)
mock_repo.save.return_value = Citation(id=1, **citation.dict())
# Act
result = service.create_citation(citation)
# Assert
assert result.id == 1
assert result.confidence == 0.85
mock_repo.save.assert_called_once()
```
### 2. 集成测试Integration Tests
| 属性 | 说明 |
|------|------|
| **目标** | 验证多个模块之间的交互是否正确 |
| **范围** | 数据库、缓存、消息队列等真实组件 |
| **速度** | 秒级,运行频率低于单元测试 |
| **工具** | Python: `pytest` + `TestClient(FastAPI)` + `testcontainers` |
| **位置** | `tests/integration/` |
| **命名** | `test_{模块A}_{模块B}_{交互场景}.py` |
**集成测试原则**
- 使用真实数据库(测试专用实例或 Docker 容器)
- 每个测试前后清理数据
- 测试事务隔离,失败时回滚
- 不测试外部 AI 平台 API使用 Mock Server
**示例**
```python
# tests/integration/test_query_citation_flow.py
class TestQueryCitationFlow:
@pytest.fixture
def client(self, test_db):
app.dependency_overrides[get_db] = lambda: test_db
return TestClient(app)
def test_should_create_query_and_detect_citations(self, client):
# 创建查询
response = client.post("/api/queries", json={
"brand": "TestBrand",
"query_template": "What is the best {brand} product?"
})
assert response.status_code == 201
query_id = response.json()["id"]
# 查询完成后应自动创建引用检测任务
# 验证数据库状态
tasks = client.get(f"/api/queries/{query_id}/tasks")
assert len(tasks.json()) > 0
```
### 3. E2E 测试End-to-End Tests
| 属性 | 说明 |
|------|------|
| **目标** | 模拟真实用户操作,验证完整业务流程 |
| **范围** | 前端 + 后端 + 数据库全流程 |
| **速度** | 分钟级CI 流水线中运行 |
| **工具** | Playwright前端+ pytest后端 API |
| **位置** | `tests/e2e/` |
| **命名** | `test_{用户旅程}_{场景}.py` |
**E2E 测试场景**
| 场景 | 说明 | 优先级 |
|------|------|--------|
| 用户注册登录 | 完整注册 → 登录 → 访问仪表盘 | P0 |
| 创建查询 | 登录 → 创建查询 → 查看执行结果 | P0 |
| 引用检测流程 | 查询执行 → 引用检测 → 查看结果 | P0 |
| 订阅升级 | 查看计划 → 升级 → 支付 → 验证权限 | P1 |
| 代理客户管理 | 登录代理账号 → 创建客户 → 执行查询 | P1 |
### 4. Agent 测试Agent Tests
| 属性 | 说明 |
|------|------|
| **目标** | 验证 AI Agent 的行为和输出质量 |
| **范围** | Agent 的输入处理和输出生成 |
| **速度** | 秒级到分钟级 |
| **工具** | `pytest` + 自定义断言 |
| **位置** | `tests/agent/` |
| **命名** | `test_{agent_name}_{场景}.py` |
**Agent 测试特殊要求**
- 使用固定的测试输入,验证输出结构和质量
- 对 AI 生成内容使用语义断言(非精确匹配)
- 评估指标准确率、召回率、F1 分数
- 定期使用回归测试集验证 Agent 性能
**示例**
```python
# tests/agent/test_citation_detector.py
class TestCitationDetectorAgent:
@pytest.fixture
def detector(self):
return CitationDetector()
def test_should_detect_brand_in_response(self, detector):
response = "ExampleBrand is a leading company in AI."
result = detector.detect(response, "ExampleBrand")
assert result.is_referenced is True
assert result.confidence >= 0.8
def test_should_not_detect_false_positive(self, detector):
response = "BrandX is a competitor, not ExampleBrand."
result = detector.detect(response, "ExampleBrand")
assert result.is_referenced is False
```
## 开发步骤规范
每个功能模块的开发必须遵循以下步骤:
### Step 1需求文档
- 阅读并理解需求文档(本文档体系中的 `01-requirements/`
- 明确功能边界、输入输出、异常场景
- 与产品经理确认需求理解无误
- 输出:功能开发任务清单
### Step 2测试用例设计
- 基于需求编写测试用例列表
- 覆盖:正常场景、边界条件、异常处理
- 使用 Given-When-Then 格式描述
- 输出:测试用例文档
```markdown
## 测试用例:引用检测功能
### TC-001: 直接引用检测
- **Given**: AI 响应包含品牌名
- **When**: 执行引用检测
- **Then**: 返回 DIRECT_QUOTE 类型,置信度 >= 0.8
### TC-002: 未引用检测
- **Given**: AI 响应不包含品牌名
- **When**: 执行引用检测
- **Then**: 返回 NO_REFERENCE 类型,置信度 = 1.0
### TC-003: 空响应处理
- **Given**: AI 响应为空字符串
- **When**: 执行引用检测
- **Then**: 抛出 ValueError
```
### Step 3编写失败测试
- 将测试用例转化为代码
- 运行测试确认失败RED
- 提交代码commit message: `test: add failing tests for citation detection`
### Step 4实现功能
- 编写最少代码使测试通过GREEN
- 不追求完美,先让测试通过
- 运行测试,确认通过
- 提交代码commit message: `feat: implement citation detection`
### Step 5重构优化
- 审视代码,消除重复和坏味道
- 应用设计模式,提升可维护性
- 持续运行测试,确保始终通过
- 提交代码commit message: `refactor: improve citation detection readability`
### Step 6集成测试
- 编写模块间的集成测试
- 验证模块协作是否正确
- 修复集成中发现的问题
- 提交代码commit message: `test: add integration tests for query-citation flow`
### Step 7更新文档
- 更新模块指南文档
- 更新 API 文档(如果有接口变更)
- 更新 CHANGELOG
- 提交代码commit message: `docs: update citation module documentation`
## 测试覆盖要求
| 层级 | 覆盖率目标 | 检查方式 |
|------|------------|----------|
| 单元测试 | >= 80% | `pytest --cov` |
| 集成测试 | 覆盖所有 API 端点 | 手动检查 |
| E2E 测试 | 覆盖所有 P0 用户旅程 | 手动检查 |
| Agent 测试 | 覆盖核心场景 | 手动检查 |
### 覆盖率排除项
以下代码不计入覆盖率统计:
- 第三方库封装代码
- 纯数据定义DTO、常量
- 自动生成的代码ORM 模型、迁移脚本)
- 调试和日志代码
## 测试运行规范
### 本地开发
```bash
# 运行所有单元测试
pytest tests/unit -v
# 运行特定模块测试
pytest tests/unit/test_citation_service.py -v
# 运行集成测试(需要本地数据库)
pytest tests/integration -v
# 运行全部测试
pytest -v
# 生成覆盖率报告
pytest --cov=app --cov-report=html
# 运行特定标记的测试
pytest -m "slow" -v
```
### CI/CD 流水线
```yaml
# .github/workflows/test.yml
jobs:
test:
steps:
- name: Run Unit Tests
run: pytest tests/unit --cov=app --cov-fail-under=80
- name: Run Integration Tests
run: pytest tests/integration
- name: Run E2E Tests
run: pytest tests/e2e
- name: Upload Coverage
uses: codecov/codecov-action@v3
```
### 测试标记Markers
| 标记 | 说明 | 运行方式 |
|------|------|----------|
| `unit` | 单元测试 | `pytest -m unit` |
| `integration` | 集成测试 | `pytest -m integration` |
| `e2e` | E2E 测试 | `pytest -m e2e` |
| `agent` | Agent 测试 | `pytest -m agent` |
| `slow` | 耗时测试 | `pytest -m "not slow"`(默认跳过) |
| `flaky` | 不稳定测试 | `pytest -m "not flaky"`(默认跳过) |
## 测试数据管理
### Fixtures
- 使用 `pytest.fixture` 管理测试数据
- 通用 fixtures 放在 `tests/conftest.py`
- 模块级 fixtures 放在测试文件内或同级 `conftest.py`
```python
# tests/conftest.py
@pytest.fixture(scope="function")
def test_db():
"""每个测试函数创建一个独立的数据库会话"""
db = TestingSessionLocal()
try:
yield db
finally:
db.rollback()
db.close()
@pytest.fixture(scope="session")
def test_user():
"""测试用户数据"""
return {
"email": "test@example.com",
"password": "TestPassword123!",
"name": "Test User"
}
```
### 工厂模式
复杂对象使用工厂函数创建:
```python
# tests/factories.py
class QueryFactory:
@staticmethod
def create(brand="TestBrand", **kwargs):
defaults = {
"brand": brand,
"query_template": f"What is the best {brand} product?",
"platforms": ["chatgpt", "kimi"]
}
defaults.update(kwargs)
return Query(**defaults)
```
## 前端测试规范
### 组件测试Jest + React Testing Library
```typescript
// components/__tests__/CitationCard.test.tsx
import { render, screen } from '@testing-library/react';
import { CitationCard } from '../CitationCard';
describe('CitationCard', () => {
it('should display citation type and confidence', () => {
// Arrange
const citation = {
type: 'direct_quote',
confidence: 0.9,
snippet: 'ExampleBrand is...'
};
// Act
render(<CitationCard citation={citation} />);
// Assert
expect(screen.getByText('直接引用')).toBeInTheDocument();
expect(screen.getByText('90%')).toBeInTheDocument();
});
});
```
### E2E 测试Playwright
```typescript
// tests/e2e/query-flow.spec.ts
import { test, expect } from '@playwright/test';
test('user can create and execute a query', async ({ page }) => {
// 登录
await page.goto('/login');
await page.fill('[name="email"]', 'test@example.com');
await page.fill('[name="password"]', 'password');
await page.click('button[type="submit"]');
// 创建查询
await page.click('text=新建查询');
await page.fill('[name="brand"]', 'TestBrand');
await page.click('text=执行查询');
// 验证结果
await expect(page.locator('text=查询执行中')).toBeVisible();
await expect(page.locator('text=检测完成')).toBeVisible({ timeout: 30000 });
});
```
## 反模式与禁忌
### 禁止事项
| 反模式 | 说明 | 后果 |
|--------|------|------|
| 不写测试直接实现 | 开发完成后再补测试 | 测试成为验证工具而非设计工具 |
| 测试依赖外部服务 | 单元测试调用真实 API | 测试不稳定、速度慢 |
| 测试间共享状态 | 测试修改全局状态 | 测试顺序依赖,偶发失败 |
| 过度 Mock | Mock 过多导致测试无意义 | 无法发现真实集成问题 |
| 测试代码重复 | 大量复制粘贴测试代码 | 维护困难,变更成本高 |
| 忽略失败测试 | 跳过或注释失败测试 | 测试套件失去保护作用 |
| 测试实现细节 | 测试私有方法和内部状态 | 重构困难,脆弱测试 |
### 测试异味Test Smells
| 异味 | 症状 | 解决方式 |
|------|------|----------|
| 神秘访客 | 测试使用未定义的魔法值 | 使用命名常量或工厂 |
| 断言轮盘 | 大量断言难以定位失败 | 拆分为独立测试 |
| 重复代码 | 多个测试有相同准备代码 | 提取为 fixture |
| 沉睡的蜗牛 | 使用 sleep 等待异步 | 使用显式等待或回调 |
| 过多前置条件 | 测试准备代码超过 10 行 | 提取辅助方法 |
---
*本文档定义 GEO 平台的 TDD 开发流程,所有开发人员必须严格遵守。*