Your Name d52509d630 docs: 完善 TDD 计划与项目名称统一
主要变更:
- 项目名称统一为"秒思智能审核平台"(替换 SmartAudit)
- 完善 TDD 实施评估与计划 (featuredoc/tdd_plan.md V2.0)
  - 新增项目现状诊断与可行性分析
  - 新增前后端测试策略与工具链配置模板
  - 新增 CI/CD 集成方案与 Codecov 配置说明
  - 标注所有待创建模板文件
- 新增 GitHub 配置脚本 (scripts/setup-github.sh)
  - 自动配置分支保护规则
  - 验证 GitHub CLI 登录状态
- 更新 TASK-005-C 包含分支保护与 Codecov 配置
- 同步更新 F-51/F-52 功能至所有相关文档
- UI 设计 Logo 统一为"秒思"

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 11:08:59 +08:00

1626 lines
48 KiB
Markdown
Raw Permalink 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.

# TDD 实施评估与计划
| 文档类型 | **TDD Implementation Assessment & Plan (测试驱动开发评估与计划)** |
| --- | --- |
| **项目名称** | 秒思智能审核平台 (AI 营销内容合规审核平台) |
| **版本号** | V2.0 |
| **发布日期** | 2026-02-03 |
| **关联文档** | tasks.md, DevelopmentPlan.md, FeatureSummary.md |
---
## 执行摘要
本文档对「秒思智能审核平台」实施 TDD测试驱动开发进行全面评估包含现状诊断、可行性分析、前后端测试策略及具体实施计划。
**核心结论:**
- **TDD 可行性评分8.2/10** — 强烈推荐实施
- **项目阶段:** Early-stage后端零代码前端组件库已建立
- **最佳时机:** ✅ 现在是实施 TDD 的理想时机(技术债为零)
---
## 目录
1. [项目现状诊断](#1-项目现状诊断)
2. [TDD 可行性分析](#2-tdd-可行性分析)
3. [前端测试策略](#3-前端测试策略)
4. [后端测试策略](#4-后端测试策略)
5. [测试分层与覆盖要求](#5-测试分层与覆盖要求)
6. [工具链配置方案](#6-工具链配置方案)
7. [CI/CD 集成方案](#7-cicd-集成方案)
8. [实施路线图](#8-实施路线图)
9. [风险与应对](#9-风险与应对)
10. [验收标准与成功指标](#10-验收标准与成功指标)
---
## 1. 项目现状诊断
### 1.1 代码库结构
```
/video-compliance-ai
├── frontend/ # 前端代码库
│ ├── components/ # React 组件库 (12个组件)
│ │ ├── ui/ # 基础 UI 组件 (7个)
│ │ ├── layout/ # 布局组件 (2个)
│ │ └── navigation/ # 导航组件 (3个)
│ ├── constants/ # 常量定义 (3个)
│ ├── styles/ # 全局样式
│ └── [配置文件]
├── backend/ # ❌ 不存在 - 待创建
├── featuredoc/ # 文档目录
└── [设计与需求文档]
```
### 1.2 技术栈现状
| 层级 | 技术栈 | 代码状态 | 测试工具状态 |
|------|--------|----------|-------------|
| **前端** | Next.js 14 + React 18 + TypeScript 5.3 | ✅ 组件库已实现 | ⚠️ 依赖已安装,配置待创建 |
| **后端** | FastAPI + Celery + Redis | ❌ 目录不存在 | ❌ 全部待创建 |
| **数据库** | PostgreSQL + pgvector | ❌ 未实现 | ❌ 全部待创建 |
| **AI 集成** | 豆包/Qwen/DeepSeek API | ❌ 未实现 | ❌ 全部待创建 |
### 1.3 测试基础设施现状
> ⚠️ **注意**:前端测试依赖已在 package.json 中声明,但 **测试环境尚不可用**,需先创建配置文件。
| 检查项 | 前端 | 后端 |
|--------|------|------|
| **测试依赖** | ✅ 已声明 (vitest, RTL, coverage-v8) | ❌ 待创建 |
| **配置文件** | ❌ vitest.config.ts 缺失 | ❌ pytest.ini 缺失 |
| **测试目录** | ❌ 无 __tests__ 目录 | ❌ 无 tests 目录 |
| **测试文件** | ❌ 0 个测试文件 | ❌ 0 个测试文件 |
| **CI/CD** | ❌ 未配置 | ❌ 未配置 |
| **可直接运行测试** | ❌ 否 | ❌ 否 |
### 1.4 前端组件清单
| 组件 | 路径 | 复杂度 | 可测性 | 关键特性 |
|------|------|--------|--------|---------|
| Button | ui/Button.tsx | 🟢 低 | 10/10 | 多 variant、loading 状态 |
| Card | ui/Card.tsx | 🟢 低 | 10/10 | 子组件组合模式 |
| Input | ui/Input.tsx | 🟡 中 | 8/10 | forwardRef、icon |
| Select | ui/Select.tsx | 🟡 中 | 8/10 | forwardRef、options |
| Modal | ui/Modal.tsx | 🟠 高 | 6/10 | useEffect 副作用、ESC 监听 |
| ProgressBar | ui/ProgressBar.tsx | 🟡 中 | 8/10 | SVG 数学计算 |
| Tag | ui/Tag.tsx | 🟢 低 | 10/10 | 状态标签映射 |
| Sidebar | navigation/Sidebar.tsx | 🟡 中 | 8/10 | 递归导航、active 状态 |
| BottomNav | navigation/BottomNav.tsx | 🟡 中 | 8/10 | badge 计数 |
| StatusBar | navigation/StatusBar.tsx | 🟢 低 | 10/10 | 静态展示 |
| DesktopLayout | layout/DesktopLayout.tsx | 🟢 低 | 8/10 | 布局组合 |
| MobileLayout | layout/MobileLayout.tsx | 🟡 中 | 8/10 | 条件渲染 |
**前端组件平均可测性8.5/10**
---
## 2. TDD 可行性分析
### 2.1 可行性评分
| 维度 | 评分 | 说明 |
|------|------|------|
| **工具链完整性** | 9/10 | Vitest + RTL + Coverage 完整,缺配置文件 |
| **代码可测性** | 8.5/10 | 大部分组件无外部依赖Modal 需特殊处理 |
| **架构合理性** | 9/10 | 前后端分离清晰,组件职责单一 |
| **文档完善度** | 9/10 | 需求、设计、任务文档完整 |
| **技术债务** | 10/10 | 后端零代码,无历史包袱 |
| **团队准备** | 7/10 | 规划完善,需建立示范代码 |
**总体可行性8.2/10 — 强烈推荐实施 TDD**
### 2.2 有利因素
| 因素 | 说明 |
|------|------|
| ✅ **新项目优势** | 后端从零开始,可完全按 TDD 流程开发 |
| ✅ **组件无副作用** | 大部分前端组件为纯函数式Props in → JSX out |
| ✅ **TypeScript 严格模式** | 类型安全,便于生成测试 fixtures |
| ✅ **清晰的接口定义** | 所有组件 Props 有完整类型定义 |
| ✅ **测试依赖已声明** | Vitest + RTL 已在 package.json 中(需创建配置文件后方可运行) |
| ✅ **路径别名完整** | @/components 等别名便于 import 和 mock |
### 2.3 挑战与应对
| 挑战 | 风险等级 | 应对策略 |
|------|---------|---------|
| AI 模型 API mock | 🔴 高 | 建立统一的 AI Mock 库 + 标注测试集 |
| Celery 异步任务测试 | 🔴 高 | 使用 celery.contrib.testing + eager 模式 |
| WebSocket 实时测试 | 🟡 中 | 使用 socket.io-client mock + 事件模拟 |
| pgvector 向量检索 | 🟡 中 | 使用 testcontainers 运行真实 PostgreSQL |
| Modal 副作用清理 | 🟢 低 | RTL render + act() + cleanup 验证 |
---
## 3. 前端测试策略
### 3.1 测试金字塔
```
┌─────────┐
│ E2E │ ← Playwright (核心用户路径)
/│ Tests │\
/ └─────────┘ \
/ 5% \
/ ┌───────────┐ \
/ │Integration│ \ ← 组件组合测试
/ │ Tests │ \
/ └───────────┘ \
/ 15% \
/ ┌─────────────────┐ \
/ │ Unit Tests │ \ ← Vitest + RTL
/ │ (Components) │ \
/ └─────────────────┘ \
/ 80% \
└─────────────────────────────────────┘
```
### 3.2 前端测试类型
#### 3.2.1 单元测试Unit Tests
**目标:** 验证单个组件的渲染、props 响应、事件处理
**工具:** Vitest + React Testing Library
**覆盖范围:**
- 所有 UI 基础组件Button, Card, Input, Select, Modal, ProgressBar, Tag
- 所有导航组件Sidebar, BottomNav, StatusBar
- 所有布局组件DesktopLayout, MobileLayout
- 常量模块colors, icons
**测试模式:**
```typescript
// 基础渲染测试
it('renders with correct variant', () => {
render(<Button variant="primary">Click</Button>);
expect(screen.getByRole('button')).toHaveClass('bg-accent-indigo');
});
// Props 响应测试
it('shows loading state when loading prop is true', () => {
render(<Button loading>Submit</Button>);
expect(screen.getByRole('button')).toBeDisabled();
});
// 事件处理测试
it('calls onClick when clicked', () => {
const handleClick = vi.fn();
render(<Button onClick={handleClick}>Click</Button>);
fireEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
```
#### 3.2.2 组件集成测试Integration Tests
**目标:** 验证组件组合行为、状态传递、用户交互流程
**工具:** Vitest + RTL + MSW (Mock Service Worker)
**覆盖范围:**
- 表单组件组合Input + Select + Button
- 布局组件嵌套Layout + Sidebar + Content
- 模态框交互流程(触发 → 显示 → 关闭)
- 导航状态同步Sidebar active state
**测试模式:**
```typescript
// 表单提交流程
it('submits form with correct data', async () => {
render(<ReviewForm onSubmit={mockSubmit} />);
await userEvent.type(screen.getByLabelText('标题'), '测试视频');
await userEvent.selectOptions(screen.getByLabelText('类型'), 'video');
await userEvent.click(screen.getByRole('button', { name: '提交' }));
expect(mockSubmit).toHaveBeenCalledWith({
title: '测试视频',
type: 'video'
});
});
```
#### 3.2.3 E2E 测试End-to-End Tests
**目标:** 验证完整用户路径、跨页面交互、真实 API 集成
**工具:** Playwright
**覆盖范围:**
- 用户登录 → 首页 → 上传视频 → 查看审核结果
- 代理商审核流程(待审核列表 → 审核详情 → 通过/拒绝)
- 品牌方终审流程(终审列表 → 审核 → 确认)
- Brief 上传与解析流程
- AI 配置修改流程
**测试模式:**
```typescript
// 视频上传到审核完成的完整流程
test('creator uploads video and receives review result', async ({ page }) => {
await page.goto('/login');
await page.fill('[name="email"]', 'creator@test.com');
await page.fill('[name="password"]', 'password123');
await page.click('button[type="submit"]');
await page.goto('/upload');
await page.setInputFiles('input[type="file"]', 'fixtures/test-video.mp4');
await page.click('button:has-text("开始审核")');
// 等待审核完成WebSocket 推送)
await expect(page.locator('.review-status')).toHaveText('审核完成', { timeout: 60000 });
// 验证审核报告
await page.click('button:has-text("查看报告")');
await expect(page.locator('.report-summary')).toBeVisible();
});
```
### 3.3 前端测试自动化方案
#### 3.3.1 测试文件组织
```
frontend/
├── components/
│ ├── ui/
│ │ ├── Button.tsx
│ │ ├── Button.test.tsx # 组件同级测试文件
│ │ ├── Card.tsx
│ │ ├── Card.test.tsx
│ │ └── ...
│ └── __tests__/ # 集成测试目录
│ ├── forms.integration.test.tsx
│ └── navigation.integration.test.tsx
├── e2e/ # E2E 测试目录
│ ├── auth.spec.ts
│ ├── upload.spec.ts
│ └── review.spec.ts
├── __mocks__/ # Mock 文件目录
│ ├── api.ts
│ ├── socket.ts
│ └── uppy.ts
└── vitest.config.ts
```
#### 3.3.2 自动化执行策略
| 触发时机 | 执行测试 | 超时限制 | 阻断条件 |
|---------|---------|---------|---------|
| 文件保存 | 相关单元测试 | 10s | 无 |
| git commit | 全部单元测试 | 2min | 失败则阻止提交 |
| PR 创建 | 单元 + 集成测试 | 5min | 失败则阻止合并 |
| PR 合并到 main | 全部测试 + E2E | 15min | 失败则回滚 |
| 每日定时 | E2E 全量回归 | 30min | 报告告警 |
### 3.4 特殊组件测试策略
#### 3.4.1 Modal 组件(副作用处理)
```typescript
import { render, screen, fireEvent, act } from '@testing-library/react';
describe('Modal', () => {
afterEach(() => {
// 验证副作用清理
expect(document.body.style.overflow).toBe('');
});
it('locks body scroll when open', () => {
render(<Modal isOpen={true} onClose={vi.fn()}>Content</Modal>);
expect(document.body.style.overflow).toBe('hidden');
});
it('closes on ESC key press', async () => {
const onClose = vi.fn();
render(<Modal isOpen={true} onClose={onClose}>Content</Modal>);
await act(async () => {
fireEvent.keyDown(document, { key: 'Escape' });
});
expect(onClose).toHaveBeenCalled();
});
it('removes event listeners on unmount', () => {
const { unmount } = render(<Modal isOpen={true} onClose={vi.fn()}>Content</Modal>);
const removeEventListenerSpy = vi.spyOn(document, 'removeEventListener');
unmount();
expect(removeEventListenerSpy).toHaveBeenCalledWith('keydown', expect.any(Function));
});
});
```
#### 3.4.2 forwardRef 组件Input, Select
```typescript
import { render, screen } from '@testing-library/react';
import { useRef } from 'react';
describe('Input with ref', () => {
it('forwards ref to input element', () => {
const TestComponent = () => {
const inputRef = useRef<HTMLInputElement>(null);
return (
<>
<Input ref={inputRef} placeholder="test" />
<button onClick={() => inputRef.current?.focus()}>Focus</button>
</>
);
};
render(<TestComponent />);
fireEvent.click(screen.getByText('Focus'));
expect(screen.getByPlaceholderText('test')).toHaveFocus();
});
});
```
#### 3.4.3 ProgressBar数学计算验证
```typescript
describe('ProgressBar', () => {
it('calculates width percentage correctly', () => {
const { container } = render(<ProgressBar value={75} max={100} />);
const progressFill = container.querySelector('.progress-fill');
expect(progressFill).toHaveStyle({ width: '75%' });
});
it('handles edge cases', () => {
// 0%
const { rerender, container } = render(<ProgressBar value={0} max={100} />);
expect(container.querySelector('.progress-fill')).toHaveStyle({ width: '0%' });
// 100%
rerender(<ProgressBar value={100} max={100} />);
expect(container.querySelector('.progress-fill')).toHaveStyle({ width: '100%' });
// 超过 100%(应该限制在 100%
rerender(<ProgressBar value={150} max={100} />);
expect(container.querySelector('.progress-fill')).toHaveStyle({ width: '100%' });
});
});
```
---
## 4. 后端测试策略
### 4.1 测试金字塔
```
┌─────────┐
│ E2E │ ← API 全流程测试
/│ Tests │\
/ └─────────┘ \
/ 5% \
/ ┌───────────┐ \
/ │Integration│ \ ← 数据库 + 外部服务
/ │ Tests │ \
/ └───────────┘ \
/ 20% \
/ ┌─────────────────┐ \
/ │ Unit Tests │ \ ← 业务逻辑
/ │ │ \
/ └─────────────────┘ \
/ 75% \
└─────────────────────────────────────┘
```
### 4.2 后端测试类型
#### 4.2.1 单元测试Unit Tests
**目标:** 验证纯业务逻辑函数、数据转换、规则引擎
**工具:** pytest + pytest-asyncio
**覆盖范围:**
- Brief 解析规则提取逻辑
- 违禁词匹配算法
- 语境理解判断逻辑
- 时间戳对齐算法
- 审核状态机
- 权限校验逻辑
**测试模式:**
```python
# tests/unit/test_brief_parser.py
import pytest
from app.services.brief_parser import extract_rules
class TestBriefParser:
def test_extract_forbidden_words(self):
"""从 Brief 中提取违禁词列表"""
brief_text = "禁止出现竞品A、竞品B、敏感词C"
rules = extract_rules(brief_text)
assert "竞品A" in rules.forbidden_words
assert "竞品B" in rules.forbidden_words
assert "敏感词C" in rules.forbidden_words
def test_extract_duration_requirement(self):
"""从 Brief 中提取时长要求"""
brief_text = "视频时长要求30秒-60秒"
rules = extract_rules(brief_text)
assert rules.min_duration == 30
assert rules.max_duration == 60
@pytest.mark.parametrize("input_text,expected", [
("必须出现品牌 Logo", True),
("建议出现品牌 Logo", False),
("", False),
])
def test_detect_logo_requirement(self, input_text, expected):
"""检测是否有 Logo 强制要求"""
rules = extract_rules(input_text)
assert rules.logo_required == expected
```
#### 4.2.2 接口测试API Tests
**目标:** 验证 API 契约、请求/响应格式、错误处理、认证授权
**工具:** pytest + httpx + pytest-asyncio
**覆盖范围:**
- 所有 P0 API 接口 100% 覆盖
- 认证流程(登录、刷新 token、登出
- 文件上传接口
- 审核提交与结果查询接口
- AI 配置接口
- WebSocket 连接与消息格式
**测试模式:**
```python
# tests/api/test_review_api.py
import pytest
from httpx import AsyncClient
from app.main import app
@pytest.fixture
async def client():
async with AsyncClient(app=app, base_url="http://test") as ac:
yield ac
@pytest.fixture
async def auth_headers(client):
"""获取认证 token"""
response = await client.post("/api/auth/login", json={
"email": "test@example.com",
"password": "password123"
})
token = response.json()["access_token"]
return {"Authorization": f"Bearer {token}"}
class TestReviewAPI:
async def test_submit_review_success(self, client, auth_headers):
"""提交审核请求成功"""
response = await client.post(
"/api/reviews",
headers=auth_headers,
json={
"video_url": "https://example.com/video.mp4",
"brief_id": "brief-123"
}
)
assert response.status_code == 201
data = response.json()
assert "review_id" in data
assert data["status"] == "pending"
async def test_submit_review_unauthorized(self, client):
"""未认证用户提交审核应返回 401"""
response = await client.post("/api/reviews", json={
"video_url": "https://example.com/video.mp4"
})
assert response.status_code == 401
async def test_submit_review_invalid_video_format(self, client, auth_headers):
"""提交不支持的视频格式应返回 400"""
response = await client.post(
"/api/reviews",
headers=auth_headers,
json={
"video_url": "https://example.com/video.exe",
"brief_id": "brief-123"
}
)
assert response.status_code == 400
assert "不支持的视频格式" in response.json()["detail"]
```
#### 4.2.3 集成测试Integration Tests
**目标:** 验证服务间协作、数据库操作、外部 API 调用、异步任务
**工具:** pytest + testcontainers + celery.contrib.testing
**覆盖范围:**
- 数据库 CRUD 操作
- Redis 缓存与消息队列
- Celery 任务链执行
- AI 模型 API 调用mock
- 文件存储OSS/S3 mock
**测试模式:**
```python
# tests/integration/test_review_pipeline.py
import pytest
from testcontainers.postgres import PostgresContainer
from app.services.review_pipeline import ReviewPipeline
from app.models import Review, ReviewStatus
from unittest.mock import AsyncMock, patch
@pytest.fixture(scope="module")
def postgres():
with PostgresContainer("postgres:14") as postgres:
yield postgres
class TestReviewPipeline:
@patch("app.services.ai_client.AIClient.analyze_video")
async def test_full_pipeline_execution(self, mock_ai, postgres):
"""完整审核流水线执行测试"""
# 配置 AI mock 返回
mock_ai.return_value = {
"violations": [],
"score": 95,
"summary": "内容合规"
}
pipeline = ReviewPipeline(db_url=postgres.get_connection_url())
# 提交审核
review_id = await pipeline.submit(
video_url="https://example.com/test.mp4",
brief_id="brief-123",
user_id="user-456"
)
# 执行审核
await pipeline.execute(review_id)
# 验证结果
review = await pipeline.get_result(review_id)
assert review.status == ReviewStatus.COMPLETED
assert review.score == 95
assert len(review.violations) == 0
```
#### 4.2.4 AI 模型测试
**目标:** 验证 AI 模型封装、Prompt 模板、规则验证、回归测试
**工具:** pytest + 标注测试集 + golden files
**覆盖范围:**
- P0 规则样本覆盖 ≥ 90%
- P1 规则样本覆盖 ≥ 70%
- 回归测试集 100% 通过
- Prompt 模板渲染正确性
**测试模式:**
```python
# tests/ai/test_violation_detection.py
import pytest
import json
from pathlib import Path
from app.services.ai_analyzer import ViolationDetector
# 加载标注测试集
FIXTURES_DIR = Path(__file__).parent / "fixtures"
with open(FIXTURES_DIR / "violation_cases.json") as f:
VIOLATION_CASES = json.load(f)
class TestViolationDetection:
@pytest.fixture
def detector(self):
return ViolationDetector(model="mock")
@pytest.mark.parametrize("case", VIOLATION_CASES["p0_rules"])
def test_p0_violation_detection(self, detector, case):
"""P0 规则违规检测测试"""
result = detector.detect(
text=case["input_text"],
rules=case["rules"]
)
assert result.has_violation == case["expected_violation"]
if case["expected_violation"]:
assert case["expected_type"] in [v.type for v in result.violations]
@pytest.mark.parametrize("case", VIOLATION_CASES["context_understanding"])
def test_context_understanding(self, detector, case):
"""语境理解测试(避免误报)"""
result = detector.detect(
text=case["input_text"],
rules=case["rules"],
context=case.get("context")
)
# 验证语境理解能力,不应误报
assert result.has_violation == case["expected_violation"]
assert result.false_positive_rate <= 0.05 # ≤ 5% 误报率
def test_regression_suite(self, detector):
"""回归测试套件"""
with open(FIXTURES_DIR / "regression_cases.json") as f:
regression_cases = json.load(f)
for case in regression_cases:
result = detector.detect(
text=case["input_text"],
rules=case["rules"]
)
assert result.has_violation == case["expected"], \
f"Regression failed for case: {case['id']}"
```
### 4.3 Mock 策略
#### 4.3.1 AI API Mock
```python
# tests/mocks/ai_mock.py
from unittest.mock import AsyncMock
import json
from pathlib import Path
class AIMockFactory:
"""AI API Mock 工厂"""
FIXTURES = Path(__file__).parent / "fixtures"
@classmethod
def create_doubao_mock(cls) -> AsyncMock:
"""创建豆包 API mock"""
mock = AsyncMock()
mock.chat.completions.create.return_value = cls._load_response("doubao_response.json")
return mock
@classmethod
def create_qwen_mock(cls) -> AsyncMock:
"""创建通义千问 API mock"""
mock = AsyncMock()
mock.chat.completions.create.return_value = cls._load_response("qwen_response.json")
return mock
@classmethod
def _load_response(cls, filename: str):
with open(cls.FIXTURES / filename) as f:
return json.load(f)
# 使用示例
@pytest.fixture
def mock_ai_client():
with patch("app.services.ai_client.get_client") as mock:
mock.return_value = AIMockFactory.create_doubao_mock()
yield mock
```
#### 4.3.2 Celery 任务 Mock
```python
# tests/conftest.py
import pytest
from celery.contrib.testing.app import TestApp
from celery.contrib.testing.worker import start_worker
@pytest.fixture(scope="module")
def celery_app():
"""创建测试用 Celery 应用"""
app = TestApp()
app.conf.update(
task_always_eager=True, # 同步执行任务
task_eager_propagates=True,
)
return app
@pytest.fixture
def celery_worker(celery_app):
"""启动测试 worker"""
with start_worker(celery_app, perform_ping_check=False) as worker:
yield worker
```
---
## 5. 测试分层与覆盖要求
### 5.1 覆盖率目标
| 层级 | 目标 | 工具 | 覆盖要求 | 门槛策略 |
|------|------|------|---------|---------|
| **前端单元测试** | 组件渲染与交互 | Vitest + RTL | ≥ 70% | PR 阻断 |
| **前端集成测试** | 组件组合行为 | Vitest + RTL | 关键流程 100% | PR 阻断 |
| **前端 E2E** | 用户路径 | Playwright | 核心路径覆盖 | 定时回归 |
| **后端单元测试** | 业务逻辑 | pytest | ≥ 80% | PR 阻断 |
| **后端接口测试** | API 契约 | pytest + httpx | P0 接口 100% | PR 阻断 |
| **后端集成测试** | 服务协作 | pytest + testcontainers | 关键链路 100% | PR 阻断 |
| **AI 模型测试** | 规则验证 | pytest + 标注集 | P0 ≥ 90%, P1 ≥ 70%, 回归 100% | PR 阻断 |
### 5.2 覆盖率豁免
以下代码可豁免覆盖率检查:
```python
# pytest 配置
[tool.coverage.run]
omit = [
"*/migrations/*", # 数据库迁移脚本
"*/__init__.py", # 空初始化文件
"*/tests/*", # 测试代码本身
"*/conftest.py", # pytest 配置
"*/main.py", # 入口文件(仅启动逻辑)
]
```
```typescript
// vitest 配置
coverage: {
exclude: [
'**/node_modules/**',
'**/dist/**',
'**/*.d.ts',
'**/types/**',
'**/index.ts', // 仅导出的聚合文件
]
}
```
---
## 6. 工具链配置方案
> 📋 **模板文件**:本章所有配置均为**待创建模板**,在执行对应 TASK 时需按此创建。
### 6.1 前端配置
> 前端目录 `frontend/` 已存在,但以下配置文件待创建。
#### 6.1.1 vitest.config.ts (待创建)
```typescript
// frontend/vitest.config.ts [待创建]
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['./vitest.setup.ts'],
include: ['**/*.{test,spec}.{ts,tsx}'],
exclude: ['**/node_modules/**', '**/e2e/**'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html', 'lcov'],
reportsDirectory: './coverage',
thresholds: {
global: {
branches: 70,
functions: 70,
lines: 70,
statements: 70,
},
},
exclude: [
'**/node_modules/**',
'**/*.d.ts',
'**/types/**',
'**/index.ts',
],
},
},
resolve: {
alias: {
'@': path.resolve(__dirname, './'),
'@/components': path.resolve(__dirname, './components'),
'@/constants': path.resolve(__dirname, './constants'),
'@/styles': path.resolve(__dirname, './styles'),
'@/lib': path.resolve(__dirname, './lib'),
'@/hooks': path.resolve(__dirname, './hooks'),
'@/types': path.resolve(__dirname, './types'),
},
},
});
```
#### 6.1.2 vitest.setup.ts (待创建)
```typescript
// frontend/vitest.setup.ts [待创建]
import '@testing-library/jest-dom';
import { cleanup } from '@testing-library/react';
import { afterEach, vi } from 'vitest';
// 每个测试后自动清理
afterEach(() => {
cleanup();
});
// Mock window.matchMedia
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
});
// Mock ResizeObserver
global.ResizeObserver = vi.fn().mockImplementation(() => ({
observe: vi.fn(),
unobserve: vi.fn(),
disconnect: vi.fn(),
}));
```
#### 6.1.3 Playwright 配置 (待创建)
```typescript
// frontend/playwright.config.ts [待创建]
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [
['html', { open: 'never' }],
['json', { outputFile: 'playwright-report.json' }],
],
use: {
baseURL: process.env.BASE_URL || 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});
```
### 6.2 后端配置
> 📋 **模板文件**:以下配置为**待创建模板**`backend/` 目录当前不存在。
> 在执行 TASK-001后端框架搭建需按此模板创建对应文件。
#### 6.2.1 pytest.ini (待创建)
```ini
# backend/pytest.ini [待创建]
[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
asyncio_mode = auto
addopts =
-v
--tb=short
--strict-markers
--cov=app
--cov-report=term-missing
--cov-report=html:coverage_html
--cov-report=xml:coverage.xml
--cov-fail-under=80
markers =
slow: marks tests as slow (deselect with '-m "not slow"')
integration: marks tests as integration tests
e2e: marks tests as end-to-end tests
filterwarnings =
ignore::DeprecationWarning
```
#### 6.2.2 conftest.py (待创建)
```python
# backend/tests/conftest.py [待创建]
import pytest
import asyncio
from typing import AsyncGenerator
from httpx import AsyncClient
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from app.main import app
from app.database import Base, get_db
from app.config import settings
# 测试数据库 URL
TEST_DATABASE_URL = "postgresql+asyncpg://test:test@localhost:5432/test_db"
@pytest.fixture(scope="session")
def event_loop():
"""创建事件循环"""
loop = asyncio.get_event_loop_policy().new_event_loop()
yield loop
loop.close()
@pytest.fixture(scope="session")
async def engine():
"""创建测试数据库引擎"""
engine = create_async_engine(TEST_DATABASE_URL, echo=True)
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
yield engine
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
await engine.dispose()
@pytest.fixture
async def db_session(engine) -> AsyncGenerator[AsyncSession, None]:
"""创建数据库会话"""
async_session = sessionmaker(
engine, class_=AsyncSession, expire_on_commit=False
)
async with async_session() as session:
yield session
await session.rollback()
@pytest.fixture
async def client(db_session) -> AsyncGenerator[AsyncClient, None]:
"""创建测试客户端"""
async def override_get_db():
yield db_session
app.dependency_overrides[get_db] = override_get_db
async with AsyncClient(app=app, base_url="http://test") as ac:
yield ac
app.dependency_overrides.clear()
@pytest.fixture
def mock_ai_response():
"""AI 响应 mock 数据"""
return {
"violations": [],
"score": 95,
"summary": "内容合规",
"details": {
"forbidden_words": [],
"logo_detected": True,
"duration_valid": True,
}
}
```
#### 6.2.3 pyproject.toml (待创建)
```toml
# backend/pyproject.toml [待创建]
[tool.poetry]
name = "miaosi-backend"
version = "1.0.0"
description = "秒思智能审核平台后端服务"
[tool.poetry.dependencies]
python = "^3.11"
fastapi = "^0.109.0"
uvicorn = "^0.27.0"
celery = "^5.3.0"
redis = "^5.0.0"
sqlalchemy = "^2.0.0"
asyncpg = "^0.29.0"
httpx = "^0.26.0"
pydantic = "^2.5.0"
python-jose = "^3.3.0"
passlib = "^1.7.4"
[tool.poetry.group.dev.dependencies]
pytest = "^8.0.0"
pytest-asyncio = "^0.23.0"
pytest-cov = "^4.1.0"
httpx = "^0.26.0"
testcontainers = "^3.7.0"
factory-boy = "^3.3.0"
faker = "^22.0.0"
respx = "^0.20.0"
[tool.coverage.run]
source = ["app"]
branch = true
omit = [
"*/migrations/*",
"*/__init__.py",
"*/tests/*",
]
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"raise NotImplementedError",
"if TYPE_CHECKING:",
]
```
---
## 7. CI/CD 集成方案
> 📋 **模板文件**:以下 GitHub Actions 工作流为**待创建模板**`.github/workflows/` 目录当前不存在。
> 在执行 TASK-005-CCI/CD 配置)时,需按此模板创建对应文件。
### 7.0 前置条件Codecov 配置
> ⚠️ **必须先完成**:工作流中使用了 `fail_ci_if_error: true`,如果未配置 CodecovCI 会直接失败。
**配置步骤:**
1. **注册 Codecov 账号**
- 访问 https://codecov.io
- 使用 GitHub 账号登录
2. **添加仓库**
- 在 Codecov Dashboard 中点击 "Add Repository"
- 选择本项目仓库并授权
3. **获取 Upload Token**(私有仓库必需)
- 进入仓库设置页:`https://codecov.io/gh/{owner}/{repo}/settings`
- 复制 "Repository Upload Token"
4. **配置 GitHub Secrets**
```bash
# 方式一GitHub Web UI
# 仓库 → Settings → Secrets and variables → Actions → New repository secret
# Name: CODECOV_TOKEN
# Value: <粘贴上一步的 token>
# 方式二GitHub CLI
gh secret set CODECOV_TOKEN --body "<your-token>"
```
5. **更新工作流**(可选,公开仓库可跳过)
```yaml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }} # 添加此行
files: ./frontend/coverage/lcov.info
flags: frontend
fail_ci_if_error: true
```
**如果暂不使用 Codecov** 将 `fail_ci_if_error: true` 改为 `false`CI 不会因覆盖率上传失败而阻断。
---
### 7.1 GitHub Actions 工作流
#### 7.1.1 前端测试工作流 (待创建)
```yaml
# .github/workflows/frontend-test.yml [待创建]
name: Frontend Tests
on:
push:
branches: [main, develop]
paths:
- 'frontend/**'
pull_request:
branches: [main, develop]
paths:
- 'frontend/**'
jobs:
unit-test:
name: Unit & Integration Tests
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./frontend
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint
- name: Run type check
run: npm run type-check
- name: Run unit tests
run: npm run test:coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }} # 见 7.0 配置说明
files: ./frontend/coverage/lcov.info
flags: frontend
fail_ci_if_error: true # 未配置 token 时改为 false
e2e-test:
name: E2E Tests
runs-on: ubuntu-latest
needs: unit-test
defaults:
run:
working-directory: ./frontend
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps
- name: Run E2E tests
run: npm run test:e2e
- name: Upload Playwright report
uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: frontend/playwright-report/
retention-days: 7
```
#### 7.1.2 后端测试工作流 (待创建)
```yaml
# .github/workflows/backend-test.yml [待创建]
name: Backend Tests
on:
push:
branches: [main, develop]
paths:
- 'backend/**'
pull_request:
branches: [main, develop]
paths:
- 'backend/**'
jobs:
test:
name: Unit & Integration Tests
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./backend
services:
postgres:
image: postgres:14
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: test_db
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install Poetry
uses: snok/install-poetry@v1
with:
version: 1.7.0
virtualenvs-create: true
virtualenvs-in-project: true
- name: Load cached venv
id: cached-poetry-dependencies
uses: actions/cache@v3
with:
path: backend/.venv
key: venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }}
- name: Install dependencies
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
run: poetry install --no-interaction --no-root
- name: Run linter
run: poetry run ruff check .
- name: Run type check
run: poetry run mypy app
- name: Run tests
env:
DATABASE_URL: postgresql+asyncpg://test:test@localhost:5432/test_db
REDIS_URL: redis://localhost:6379/0
run: poetry run pytest --cov-report=xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }} # 见 7.0 配置说明
files: ./backend/coverage.xml
flags: backend
fail_ci_if_error: true # 未配置 token 时改为 false
```
### 7.2 PR 检查配置(分支保护规则)
> ⚠️ **重要提示**GitHub 分支保护规则**不能**通过配置文件自动生效,必须执行配置脚本或手动配置。
#### 方式一:自动化脚本(推荐)
项目提供了一键配置脚本,自动完成所有 GitHub 设置:
```bash
# 运行 GitHub 配置脚本
./scripts/setup-github.sh
```
**脚本功能:**
- ✅ 检查 GitHub CLI 安装和登录状态
- ✅ 自动配置分支保护规则
- ✅ 设置必需的状态检查CI 通过才能合并)
- ✅ 设置必需的 PR 审批(至少 1 人)
- ✅ 启用合并后自动删除分支
- ✅ 验证配置是否生效
**前置条件:**
```bash
# 1. 安装 GitHub CLI
brew install gh # macOS
sudo apt install gh # Ubuntu
# 2. 登录 GitHub
gh auth login
# 3. 运行配置脚本
./scripts/setup-github.sh
```
#### 方式二GitHub Web UI备选
如果脚本执行失败(如免费版 GitHub 不支持 API 配置),可手动配置:
1. 进入仓库 → **Settings** → **Branches**
2. 点击 **Add branch protection rule**
3. 配置以下选项:
| 配置项 | 设置值 |
|--------|--------|
| Branch name pattern | `main` |
| ✅ Require a pull request before merging | 启用 |
| ✅ Require approvals | 1 |
| ✅ Require status checks to pass before merging | 启用 |
| ✅ Require branches to be up to date before merging | 启用 |
| Status checks that are required | `Frontend Tests / Unit & Integration Tests`<br>`Backend Tests / Unit & Integration Tests` |
### 7.3 覆盖率报告配置 (待创建)
```yaml
# codecov.yml [待创建]
coverage:
precision: 2
round: down
range: "70...100"
status:
project:
default:
target: auto
threshold: 1%
patch:
default:
target: 80%
threshold: 1%
flags:
frontend:
paths:
- frontend/
carryforward: true
backend:
paths:
- backend/
carryforward: true
comment:
layout: "reach,diff,flags,files"
behavior: default
require_changes: true
```
---
## 8. 实施路线图
### 8.1 阶段划分
```
Phase 1: 基础设施搭建 (Week 1)
├── 配置文件创建
├── CI/CD 流水线
├── 测试示范代码
└── 团队培训
Phase 2: 前端 TDD (Week 2-3)
├── UI 组件测试
├── 导航组件测试
├── 布局组件测试
└── 集成测试
Phase 3: 后端 TDD - 基础模块 (Week 4-5)
├── 认证授权测试
├── 数据库操作测试
├── API 框架测试
└── 文件上传测试
Phase 4: 后端 TDD - 核心业务 (Week 6-9)
├── Brief 解析测试
├── 脚本预审测试
├── 视频审核流水线测试
└── AI 模型集成测试
Phase 5: E2E 与回归 (Week 10-11)
├── 核心用户路径 E2E
├── 回归测试套件
├── 性能测试
└── 上线前验收
```
### 8.2 详细任务分解
#### Phase 1: 基础设施搭建 (Week 1)
| 任务 | 产出物 | 预估工时 | 负责人 |
|------|--------|---------|--------|
| 创建 vitest.config.ts | 配置文件 | 2h | 前端开发 |
| 创建 vitest.setup.ts | 测试环境配置 | 2h | 前端开发 |
| 创建 playwright.config.ts | E2E 配置 | 2h | 前端开发 |
| 创建 pytest.ini | 后端测试配置 | 2h | 后端开发 |
| 创建 conftest.py | 测试 fixtures | 4h | 后端开发 |
| 配置 GitHub Actions - 前端 | CI/CD 流水线 | 4h | DevOps |
| 配置 GitHub Actions - 后端 | CI/CD 流水线 | 4h | DevOps |
| 配置 Codecov | 覆盖率报告 | 2h | DevOps |
| 编写 Button.test.tsx 示范 | 测试示范代码 | 4h | 前端开发 |
| 编写 Modal.test.tsx 示范 | 副作用测试示范 | 4h | 前端开发 |
| 团队 TDD 培训 | 培训材料 | 8h | Tech Lead |
**Phase 1 总工时38h**
#### Phase 2: 前端 TDD (Week 2-3)
| 任务 | 测试文件 | 预估工时 |
|------|---------|---------|
| Button 组件测试 | Button.test.tsx | 4h |
| Card 组件测试 | Card.test.tsx | 3h |
| Input 组件测试 | Input.test.tsx | 5h |
| Select 组件测试 | Select.test.tsx | 4h |
| Modal 组件测试 | Modal.test.tsx | 6h |
| ProgressBar 组件测试 | ProgressBar.test.tsx | 5h |
| Tag 组件测试 | Tag.test.tsx | 3h |
| Sidebar 组件测试 | Sidebar.test.tsx | 5h |
| BottomNav 组件测试 | BottomNav.test.tsx | 4h |
| StatusBar 组件测试 | StatusBar.test.tsx | 2h |
| Layout 组件测试 | Layout.test.tsx | 4h |
| 常量模块测试 | constants.test.ts | 2h |
| 表单集成测试 | forms.integration.test.tsx | 6h |
| 导航集成测试 | navigation.integration.test.tsx | 5h |
**Phase 2 总工时58h**
#### Phase 3-5: 后端 TDD (Week 4-11)
详见 `tasks.md` 中的后端开发任务,每个任务均需遵循 TDD 流程。
---
## 9. 风险与应对
### 9.1 技术风险
| 风险 | 概率 | 影响 | 应对策略 |
|------|------|------|---------|
| AI API 响应不稳定导致测试波动 | 高 | 高 | 使用确定性 mock + golden files |
| 异步任务测试复杂度高 | 高 | 中 | 使用 eager 模式 + 任务链 mock |
| E2E 测试运行时间过长 | 中 | 中 | 并行执行 + 关键路径优先 |
| 覆盖率目标难以达成 | 中 | 低 | 分阶段提升目标,先 60% 再 80% |
| 测试代码维护成本高 | 中 | 中 | 建立测试工具库 + 定期重构 |
### 9.2 团队风险
| 风险 | 概率 | 影响 | 应对策略 |
|------|------|------|---------|
| 团队缺乏 TDD 经验 | 高 | 高 | 先期培训 + Pair Programming |
| 赶进度跳过测试 | 高 | 高 | CI 强制检查 + 代码审查 |
| 测试质量参差不齐 | 中 | 中 | 建立测试规范 + 示范代码库 |
### 9.3 应急预案
1. **覆盖率低于门槛**:临时下调门槛,记录技术债务,安排补测 Sprint
2. **CI 流水线阻塞**:建立临时绕过机制(需 Tech Lead 审批)
3. **E2E 环境不稳定**:降级为 API 集成测试,后续修复环境
---
## 10. 验收标准与成功指标
### 10.1 TDD 实施验收标准
| 阶段 | 验收标准 | 验收方式 |
|------|---------|---------|
| Phase 1 完成 | CI/CD 流水线可运行,测试示范通过 | Demo 演示 |
| Phase 2 完成 | 前端覆盖率 ≥ 70%,所有组件有测试 | 覆盖率报告 |
| Phase 3-4 完成 | 后端覆盖率 ≥ 80%P0 API 100% 覆盖 | 覆盖率报告 |
| Phase 5 完成 | E2E 核心路径通过,回归测试 100% | 测试报告 |
### 10.2 长期成功指标
| 指标 | 目标值 | 测量方式 |
|------|--------|---------|
| **前端覆盖率** | ≥ 70% | Codecov 报告 |
| **后端覆盖率** | ≥ 80% | Codecov 报告 |
| **P0 API 测试覆盖** | 100% | 接口清单核对 |
| **AI 规则 P0 覆盖** | ≥ 90% | 标注集验证 |
| **AI 规则 P1 覆盖** | ≥ 70% | 标注集验证 |
| **回归测试通过率** | 100% | 回归测试套件 |
| **CI 成功率** | ≥ 95% | GitHub Actions 统计 |
| **上线后回归缺陷** | ≤ 2% | QA 缺陷统计 |
| **开发周期缩短** | 30% | 与历史项目对比 |
### 10.3 TDD 统一约定
- 每个任务必须包含:
- 至少 1 个失败测试用例(先写)
- 核心成功路径测试
- 关键异常路径测试
- 合并前需满足:
- 测试全部通过
- 覆盖率达标
- 关键路径无回归
- 任务完成即具备可运行测试与最小化覆盖
- 关键功能如审核台、AI 配置、上传链路)必须包含 E2E 测试
---
## 附录 A测试命名规范
### 前端测试命名
```typescript
// 文件命名:[ComponentName].test.tsx
// 例如Button.test.tsx, Modal.test.tsx
// 测试套件命名describe('[ComponentName]', ...)
// 例如describe('Button', ...)
// 测试用例命名it('[动作] when [条件]', ...)
// 例如:
it('renders primary variant when variant prop is primary', ...)
it('calls onClick when button is clicked', ...)
it('shows loading spinner when loading is true', ...)
it('disables button when disabled prop is true', ...)
```
### 后端测试命名
```python
# 文件命名test_[module_name].py
# 例如test_brief_parser.py, test_review_api.py
# 测试类命名class Test[FeatureName]:
# 例如class TestBriefParser:, class TestReviewAPI:
# 测试方法命名def test_[行为]_[条件]():
# 例如:
def test_extract_forbidden_words_from_brief():
def test_submit_review_returns_401_when_unauthorized():
def test_detect_violation_when_forbidden_word_present():
```
---
## 附录 B测试数据管理
### B.1 Fixtures 目录结构
```
tests/
├── fixtures/
│ ├── videos/ # 测试视频文件
│ │ ├── valid_video.mp4
│ │ ├── invalid_format.avi
│ │ └── oversized_video.mp4
│ ├── briefs/ # Brief 测试数据
│ │ ├── standard_brief.pdf
│ │ └── complex_brief.pdf
│ ├── ai_responses/ # AI API 响应 mock
│ │ ├── doubao_response.json
│ │ ├── qwen_response.json
│ │ └── error_response.json
│ ├── violation_cases.json # 违规检测测试集
│ └── regression_cases.json # 回归测试集
```
### B.2 Golden Files 管理
```python
# 使用 golden files 进行 AI 输出验证
def test_ai_analysis_output(snapshot):
result = ai_analyzer.analyze(test_video)
snapshot.assert_match(result.to_json(), "ai_analysis_output.json")
```
---
## 附录 C变更记录
| 版本 | 日期 | 变更内容 | 变更人 |
|------|------|---------|--------|
| V1.0 | 2026-02-03 | 初始版本,基础 TDD 框架 | - |
| V2.0 | 2026-02-03 | 完整重写增加项目诊断、前后端策略、CI/CD 方案 | Claude |
---
**文档维护:** 覆盖率阈值调整需经过 PM + 技术负责人确认;重大功能变更需同步更新本计划。