# 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(); // 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 开发流程,所有开发人员必须严格遵守。*