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

18 KiB
Raw Blame History

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.mockTypeScript: 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 开发流程,所有开发人员必须严格遵守。