18 KiB
18 KiB
GEO 平台 - TDD 流程规范
概述
本文档定义 GEO 平台的测试驱动开发(TDD)工作流程。所有功能开发必须遵循 TDD 原则:先写测试,再写实现,最后重构。
TDD 的核心价值:
- 设计先行:通过测试用例明确需求和接口设计
- 快速反馈:自动化测试提供即时的正确性验证
- 安全重构:测试覆盖确保重构不会破坏功能
- 文档即代码:测试用例本身就是最准确的接口文档
RED-GREEN-REFACTOR 循环
基本循环
┌──────────────────────┐
│ 1. RED 编写失败测试 │
│ 写一个会失败的测试用例 │
└───────────┬──────────┘
│
▼
┌──────────────────────┐
│ 2. GREEN 通过测试 │
│ 写最少代码让测试通过 │
└───────────┬──────────┘
│
▼
┌──────────────────────┐
│ 3. REFACTOR 重构代码 │
│ 优化实现,保持测试通过 │
└───────────┬──────────┘
│
└──────▶ 下一个循环
各阶段要求
RED 阶段 - 编写失败测试
- 前提:已完成需求分析和接口设计
- 动作:编写一个测试用例,验证某个具体功能点
- 验证:运行测试,确认测试失败(且失败原因符合预期)
- 原则:
- 一次只写一个测试
- 测试命名清晰表达被测行为
- 使用 Given-When-Then 结构组织测试代码
- 测试失败信息应清晰说明期望 vs 实际
示例(Python + pytest):
# 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 阶段
示例:
# 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 循环)
- 关注代码异味(重复、过长函数、魔法数字等)
重构后示例:
# 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
示例:
# 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)
示例:
# 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 性能
示例:
# 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 格式描述
- 输出:测试用例文档
## 测试用例:引用检测功能
### 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 模型、迁移脚本)
- 调试和日志代码
测试运行规范
本地开发
# 运行所有单元测试
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 流水线
# .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
# 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"
}
工厂模式
复杂对象使用工厂函数创建:
# 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)
// 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)
// 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 开发流程,所有开发人员必须严格遵守。