# TDD 实施评估与计划 | 文档类型 | **Testing Strategy (测试驱动开发实施计划)** | | --- | --- | | **项目名称** | SmartAudit (AI 营销内容合规审核平台) | | **版本号** | V1.0 | | **发布日期** | 2026-02-02 | | **关联文档** | tasks.md, DevelopmentPlan.md, FeatureSummary.md | --- ## 版本历史 (Version History) | 版本 | 日期 | 作者 | 变更说明 | | --- | --- | --- | --- | | V1.0 | 2026-02-02 | Claude | 初稿:项目诊断、TDD可行性评估、实施计划 | --- ## 目录 1. [项目现状诊断](#1-项目现状诊断) 2. [TDD 可行性评估](#2-tdd-可行性评估) 3. [测试金字塔架构](#3-测试金字塔架构) 4. [后端测试策略](#4-后端测试策略) 5. [前端测试策略](#5-前端测试策略) 6. [AI 模型测试策略](#6-ai-模型测试策略) 7. [端到端测试策略](#7-端到端测试策略) 8. [实施路线图](#8-实施路线图) 9. [测试覆盖率目标](#9-测试覆盖率目标) 10. [工具链配置](#10-工具链配置) 11. [团队规范与培训](#11-团队规范与培训) 12. [风险与挑战](#12-风险与挑战) --- ## 1. 项目现状诊断 ### 1.1 代码库状态 | 维度 | 当前状态 | 评估 | | --- | --- | --- | | **源代码** | 零代码,纯需求阶段 | ✅ 最佳TDD切入点 | | **文档完整度** | 5,796行,覆盖PRD/RD/技术架构/UI | ✅ 需求明确 | | **技术选型** | 已确定:FastAPI + Next.js | ✅ 测试生态成熟 | | **任务拆解** | 77个开发任务,优先级明确 | ✅ 粒度适合TDD | | **验收标准** | 每个功能有量化指标 | ✅ 可直接转化为测试用例 | | **CI/CD** | 已规划,待实施 | ⚠️ 需同步搭建 | ### 1.2 技术栈测试生态评估 | 技术 | 测试框架支持 | 生态成熟度 | Mock/Stub 支持 | | --- | --- | --- | --- | | **FastAPI** | pytest + httpx | ⭐⭐⭐⭐⭐ | TestClient 内置 | | **Celery** | pytest-celery | ⭐⭐⭐⭐ | eager mode 支持 | | **PostgreSQL** | TestContainers | ⭐⭐⭐⭐⭐ | 容器化隔离 | | **Next.js/React** | Vitest + RTL | ⭐⭐⭐⭐⭐ | MSW 拦截 | | **Zustand** | 原生测试支持 | ⭐⭐⭐⭐ | 无需特殊处理 | | **Socket.io** | jest-socket.io-mock | ⭐⭐⭐ | 需手动 Mock | ### 1.3 项目复杂度分析 ``` 复杂度热力图: ┌─────────────────────────────────────────────────────────┐ │ 模块 │ 业务复杂度 │ 测试难度 │ ├─────────────────────────────────────────────────────────┤ │ 认证与权限 (RBAC) │ ██░░░ │ ██░░░ │ │ Brief 解析 (LLM) │ ████░ │ ████░ │ │ 规则引擎 │ ███░░ │ ██░░░ │ │ 视频上传 (Tus) │ ██░░░ │ ███░░ │ │ ASR/OCR/CV 流水线 │ █████ │ █████ │ ← 最高 │ 多模态时间戳对齐 │ █████ │ █████ │ ← 最高 │ WebSocket 进度推送 │ ██░░░ │ ███░░ │ │ 审核决策流程 │ ███░░ │ ██░░░ │ │ 数据看板 │ ██░░░ │ ██░░░ │ │ 移动端 H5 │ ██░░░ │ ███░░ │ └─────────────────────────────────────────────────────────┘ ``` **诊断结论**: - ✅ **绿灯项目**:零代码起步,是实施TDD的理想时机 - ✅ 技术栈测试生态成熟,无明显阻碍 - ⚠️ AI流水线(ASR/OCR/CV)测试需特殊策略 --- ## 2. TDD 可行性评估 ### 2.1 综合评分 | 评估维度 | 评分 | 说明 | | --- | --- | --- | | **需求明确性** | ⭐⭐⭐⭐⭐ | PRD/RD 详尽,用户故事完整 | | **功能粒度** | ⭐⭐⭐⭐⭐ | 77个任务,边界清晰 | | **技术可测性** | ⭐⭐⭐⭐ | 主流框架,生态成熟 | | **团队规模** | ⭐⭐⭐⭐ | 8人精干团队,沟通高效 | | **时间充裕度** | ⭐⭐⭐⭐ | 11周排期,非极限压缩 | | **验收标准量化** | ⭐⭐⭐⭐⭐ | 每个功能有明确KPI | **总体评估:🟢 高度可行 (95分/100)** ### 2.2 TDD 适用性分析 | 模块类型 | TDD 适用度 | 推荐策略 | | --- | --- | --- | | **纯业务逻辑** | ⭐⭐⭐⭐⭐ | 严格 TDD(先写测试) | | **API 接口** | ⭐⭐⭐⭐⭐ | 契约测试 + TDD | | **数据模型** | ⭐⭐⭐⭐ | TDD + Schema 验证 | | **规则引擎** | ⭐⭐⭐⭐⭐ | 表格驱动测试 + TDD | | **AI 模型调用** | ⭐⭐⭐ | 混合模式(输入输出验证) | | **AI Prompt** | ⭐⭐ | 标注测试集验证 | | **UI 组件** | ⭐⭐⭐⭐ | 组件级 TDD | | **E2E 流程** | ⭐⭐ | BDD + E2E 测试 | ### 2.3 TDD 实施模式选择 推荐采用 **"分层混合 TDD"** 模式: ``` ┌──────────────────────────────────────────────────────────────┐ │ 分层混合 TDD 模式 │ ├──────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 第1层:严格 TDD (100% 覆盖) │ │ │ │ • 工具函数 (utils) │ │ │ │ • 数据验证器 (validators) │ │ │ │ • 规则引擎 (rule engine) │ │ │ │ • 业务逻辑服务 (services) │ │ │ └─────────────────────────────────────────────────────┘ │ │ ↓ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 第2层:契约优先 (Contract-First) │ │ │ │ • API 接口 → 先定义 OpenAPI │ │ │ │ • 数据模型 → 先定义 Schema │ │ │ │ • WebSocket 消息 → 先定义消息格式 │ │ │ └─────────────────────────────────────────────────────┘ │ │ ↓ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 第3层:标注集验证 (AI 模型) │ │ │ │ • ASR/OCR/CV → 标注测试集 + 阈值验证 │ │ │ │ • LLM Prompt → Few-shot 示例 + 定期回归 │ │ │ │ • 向量检索 → 召回率/精确率评估 │ │ │ └─────────────────────────────────────────────────────┘ │ │ ↓ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 第4层:行为驱动 (BDD + E2E) │ │ │ │ • 用户故事 → Playwright E2E │ │ │ │ • 关键路径 → 冒烟测试 │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ └──────────────────────────────────────────────────────────────┘ ``` --- ## 3. 测试金字塔架构 ### 3.1 测试层级分布 ``` ┌─────────────┐ │ E2E 测试 │ 5% │ (Playwright)│ └──────┬──────┘ │ ┌──────────┴──────────┐ │ 集成测试 │ 20% │ (API + DB + 外部) │ └──────────┬──────────┘ │ ┌──────────────────┴──────────────────┐ │ 单元测试 │ 75% │ (函数、类、组件、纯逻辑) │ └─────────────────────────────────────┘ ``` ### 3.2 各层级职责划分 | 层级 | 占比 | 覆盖范围 | 执行频率 | 执行时间 | | --- | --- | --- | --- | --- | | **单元测试** | 75% | 函数/类/组件/纯逻辑 | 每次提交 | < 30秒 | | **集成测试** | 20% | API/DB/消息队列/外部服务 | 每次PR | < 5分钟 | | **E2E 测试** | 5% | 完整用户流程 | 每日/发布前 | < 15分钟 | ### 3.3 SmartAudit 测试分层详情 ``` ┌────────────────────────────────────────────────────────────────────┐ │ SmartAudit 测试分层 │ ├────────────────────────────────────────────────────────────────────┤ │ │ │ 【E2E 测试层】 Playwright │ │ ├─ 达人流程:上传视频 → 等待审核 → 查看结果 → 申诉 │ │ ├─ 代理商流程:配置Brief → 审核视频 → 驳回/通过 │ │ └─ 品牌方流程:查看看板 → 配置规则 → 审批强制通过 │ │ │ │ 【集成测试层】 pytest + TestContainers │ │ ├─ API 接口测试 (httpx TestClient) │ │ ├─ 数据库集成测试 (PostgreSQL + pgvector) │ │ ├─ Redis 缓存测试 │ │ ├─ Celery 任务测试 (eager mode) │ │ ├─ 文件上传测试 (OSS Mock) │ │ └─ WebSocket 推送测试 │ │ │ │ 【单元测试层】 │ │ │ │ │ │ 后端 (pytest) 前端 (Vitest) │ │ │ ├─ 工具函数 ├─ 工具函数 │ │ │ ├─ 数据验证器 ├─ 格式化函数 │ │ │ ├─ 规则引擎逻辑 ├─ 状态管理 (Zustand) │ │ │ ├─ 时间戳对齐算法 ├─ React Hooks │ │ │ ├─ Brief 解析逻辑 ├─ UI 组件 │ │ │ ├─ 业务服务方法 └─ 表单验证逻辑 │ │ │ └─ Pydantic 模型 │ │ │ │ │ │ AI 模型 (标注测试集) │ │ │ ├─ ASR 输出格式验证 │ │ │ ├─ OCR 输出格式验证 │ │ │ ├─ CV 检测结果验证 │ │ │ ├─ LLM 输出解析验证 │ │ │ └─ 向量相似度计算验证 │ │ │ │ └────────────────────────────────────────────────────────────────────┘ ``` --- ## 4. 后端测试策略 ### 4.1 测试框架选型 | 用途 | 工具 | 说明 | | --- | --- | --- | | **测试框架** | pytest | Python 标准,插件生态丰富 | | **异步测试** | pytest-asyncio | FastAPI 异步支持 | | **覆盖率** | pytest-cov | 覆盖率报告 | | **Mock** | unittest.mock / pytest-mock | 依赖模拟 | | **Fixture** | pytest fixtures | 测试数据管理 | | **参数化** | @pytest.mark.parametrize | 表格驱动测试 | | **容器化测试** | TestContainers | DB/Redis/MQ 隔离 | | **API 测试** | httpx + TestClient | FastAPI 内置 | | **Celery 测试** | celery.contrib.testing | 任务测试 | | **快照测试** | syrupy | JSON 输出验证 | ### 4.2 目录结构 ``` backend/ ├── tests/ │ ├── __init__.py │ ├── conftest.py # 全局 fixtures │ │ │ ├── unit/ # 单元测试 (75%) │ │ ├── __init__.py │ │ ├── test_validators.py # 数据验证器 │ │ ├── test_utils.py # 工具函数 │ │ ├── test_rule_engine.py # 规则引擎 │ │ ├── test_timestamp.py # 时间戳对齐 │ │ ├── test_brief_parser.py # Brief 解析逻辑 │ │ └── services/ │ │ ├── test_auth.py │ │ ├── test_brief.py │ │ ├── test_video.py │ │ └── test_report.py │ │ │ ├── integration/ # 集成测试 (20%) │ │ ├── __init__.py │ │ ├── conftest.py # DB/Redis fixtures │ │ ├── test_api_auth.py # 认证 API │ │ ├── test_api_brief.py # Brief API │ │ ├── test_api_video.py # 视频 API │ │ ├── test_api_report.py # 报告 API │ │ ├── test_db_models.py # 数据库模型 │ │ ├── test_celery_tasks.py # 异步任务 │ │ └── test_websocket.py # WebSocket │ │ │ ├── ai/ # AI 模型测试 │ │ ├── __init__.py │ │ ├── conftest.py # 测试集加载 │ │ ├── test_asr.py # ASR 输出验证 │ │ ├── test_ocr.py # OCR 输出验证 │ │ ├── test_cv.py # CV 检测验证 │ │ ├── test_llm.py # LLM 输出解析 │ │ └── test_embedding.py # 向量生成验证 │ │ │ ├── e2e/ # 端到端测试 (5%) │ │ ├── __init__.py │ │ └── test_workflows.py # 完整流程 │ │ │ └── fixtures/ # 测试数据 │ ├── briefs/ # 测试 Brief 文件 │ ├── videos/ # 测试视频文件 │ ├── rules/ # 测试规则集 │ └── snapshots/ # 快照数据 │ ├── pytest.ini # pytest 配置 └── pyproject.toml # 项目配置 ``` ### 4.3 核心测试用例设计 #### 4.3.1 规则引擎测试 (表格驱动) ```python # tests/unit/test_rule_engine.py import pytest from app.services.rule_engine import RuleEngine class TestProhibitedWordDetection: """违禁词检测测试 - 表格驱动""" @pytest.mark.parametrize("text,expected_violations,context", [ # 广告语境下的违禁词 - 应检出 ("这是全网销量第一的产品", ["全网第一"], "advertisement"), ("我们是行业领导者", ["行业领导者"], "advertisement"), ("史上最低价促销", ["史上最低价"], "advertisement"), # 日常语境下的相同词 - 不应检出 ("今天是我最开心的一天", [], "daily_conversation"), ("这是我第一次来这里", [], "daily_conversation"), # 边界情况 ("", [], "advertisement"), ("这是一个普通的产品介绍", [], "advertisement"), # 组合违禁词 ("全网销量第一,史上最低价", ["全网第一", "史上最低价"], "advertisement"), ]) def test_prohibited_word_detection(self, text, expected_violations, context): """验证违禁词检测的准确性""" engine = RuleEngine() result = engine.detect_prohibited_words(text, context=context) assert set(result.violations) == set(expected_violations) ``` #### 4.3.2 时间戳对齐算法测试 ```python # tests/unit/test_timestamp.py import pytest from app.utils.timestamp_align import TimestampAligner class TestMultiModalAlignment: """多模态时间戳对齐测试""" @pytest.fixture def aligner(self): return TimestampAligner(tolerance_ms=500) @pytest.mark.parametrize("asr_ts,ocr_ts,cv_ts,expected_merged", [ # 完全对齐 (1000, 1000, 1000, 1000), # 容差范围内对齐 (1000, 1200, 1100, 1100), # 取中位数 # 超出容差 (1000, 2000, 3000, None), # 不合并 ]) def test_timestamp_alignment(self, aligner, asr_ts, ocr_ts, cv_ts, expected_merged): """验证时间戳对齐逻辑""" events = [ {"source": "asr", "timestamp_ms": asr_ts, "content": "test"}, {"source": "ocr", "timestamp_ms": ocr_ts, "content": "test"}, {"source": "cv", "timestamp_ms": cv_ts, "content": "logo_detected"}, ] merged = aligner.merge_events(events) if expected_merged: assert len(merged) == 1 assert merged[0]["timestamp_ms"] == expected_merged else: assert len(merged) == 3 # 未合并 def test_duration_calculation_accuracy(self, aligner): """验证时长统计误差 ≤ 0.5秒""" events = [ {"timestamp_ms": 0, "type": "product_appear"}, {"timestamp_ms": 5500, "type": "product_disappear"}, ] duration = aligner.calculate_duration(events) # 误差应 ≤ 500ms assert abs(duration - 5500) <= 500 ``` #### 4.3.3 Brief 解析测试 ```python # tests/unit/test_brief_parser.py import pytest from app.services.brief_parser import BriefParser class TestBriefParsing: """Brief 解析逻辑测试""" @pytest.fixture def parser(self): return BriefParser() def test_extract_selling_points(self, parser): """验证卖点提取""" brief_content = """ 产品核心卖点: 1. 24小时持妆 2. 天然成分 3. 敏感肌适用 """ result = parser.extract_selling_points(brief_content) assert "24小时持妆" in result.selling_points assert "天然成分" in result.selling_points assert "敏感肌适用" in result.selling_points def test_extract_prohibited_words(self, parser): """验证禁忌词提取""" brief_content = """ 禁止使用的词汇: - 药用 - 治疗 - 根治 """ result = parser.extract_prohibited_words(brief_content) assert set(result.prohibited_words) == {"药用", "治疗", "根治"} def test_conflict_detection(self, parser): """验证 Brief 与平台规则冲突检测""" brief_rules = {"allowed_words": ["最佳效果"]} platform_rules = {"prohibited_words": ["最佳"]} conflicts = parser.detect_conflicts(brief_rules, platform_rules) assert len(conflicts) == 1 assert "最佳效果" in conflicts[0]["conflicting_term"] ``` ### 4.4 集成测试策略 #### 4.4.1 数据库集成测试 ```python # tests/integration/conftest.py import pytest from testcontainers.postgres import PostgresContainer from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker @pytest.fixture(scope="session") def postgres_container(): """启动 PostgreSQL 测试容器""" with PostgresContainer("postgres:15-alpine") as postgres: yield postgres @pytest.fixture(scope="function") def db_session(postgres_container): """每个测试函数独立的数据库会话""" engine = create_engine(postgres_container.get_connection_url()) Session = sessionmaker(bind=engine) session = Session() # 创建表 Base.metadata.create_all(engine) yield session # 清理 session.rollback() session.close() ``` #### 4.4.2 API 集成测试 ```python # tests/integration/test_api_brief.py import pytest from httpx import AsyncClient from app.main import app class TestBriefAPI: """Brief API 集成测试""" @pytest.fixture async def client(self): async with AsyncClient(app=app, base_url="http://test") as ac: yield ac @pytest.fixture async def auth_headers(self, client): """获取认证头""" response = await client.post("/auth/login", json={ "username": "test_agency", "password": "password" }) token = response.json()["access_token"] return {"Authorization": f"Bearer {token}"} async def test_upload_brief_pdf(self, client, auth_headers, tmp_path): """测试 Brief PDF 上传""" # 准备测试文件 test_pdf = tmp_path / "test_brief.pdf" test_pdf.write_bytes(b"%PDF-1.4 test content") with open(test_pdf, "rb") as f: response = await client.post( "/api/v1/briefs/upload", files={"file": ("test.pdf", f, "application/pdf")}, headers=auth_headers ) assert response.status_code == 202 assert "task_id" in response.json() async def test_get_brief_parsing_result(self, client, auth_headers): """测试获取 Brief 解析结果""" # 假设已有解析完成的 Brief brief_id = "test-brief-id" response = await client.get( f"/api/v1/briefs/{brief_id}", headers=auth_headers ) assert response.status_code == 200 result = response.json() assert "selling_points" in result assert "prohibited_words" in result ``` ### 4.5 Celery 异步任务测试 ```python # tests/integration/test_celery_tasks.py import pytest from unittest.mock import patch, MagicMock from app.tasks.video_auditing import audit_video_task class TestVideoAuditingTask: """视频审核异步任务测试""" @pytest.fixture def mock_ai_services(self): """Mock 所有 AI 服务""" with patch("app.tasks.video_auditing.ASRService") as mock_asr, \ patch("app.tasks.video_auditing.OCRService") as mock_ocr, \ patch("app.tasks.video_auditing.CVService") as mock_cv: mock_asr.return_value.transcribe.return_value = { "text": "这是测试文本", "timestamps": [{"start": 0, "end": 1000, "text": "这是测试文本"}] } mock_ocr.return_value.extract.return_value = { "frames": [{"timestamp": 500, "text": "字幕内容"}] } mock_cv.return_value.detect.return_value = { "logos": [], "objects": [{"timestamp": 500, "object": "product"}] } yield {"asr": mock_asr, "ocr": mock_ocr, "cv": mock_cv} def test_video_audit_task_success(self, mock_ai_services, db_session): """测试视频审核任务成功执行""" task_id = "test-task-id" video_url = "https://test.oss.com/test.mp4" brief_id = "test-brief-id" # 使用 eager 模式同步执行 result = audit_video_task.apply( args=[task_id, video_url, brief_id] ).get() assert result["status"] == "completed" assert "report" in result assert "risk_items" in result["report"] def test_video_audit_task_with_violations(self, mock_ai_services, db_session): """测试检测到违规时的处理""" # 修改 Mock 返回值,模拟检测到违禁词 mock_ai_services["asr"].return_value.transcribe.return_value = { "text": "这是全网销量第一的产品", "timestamps": [{"start": 0, "end": 1000, "text": "这是全网销量第一的产品"}] } result = audit_video_task.apply( args=["test-task", "url", "brief-id"] ).get() assert result["status"] == "completed" assert len(result["report"]["risk_items"]) > 0 assert any( item["type"] == "prohibited_word" for item in result["report"]["risk_items"] ) ``` --- ## 5. 前端测试策略 ### 5.1 测试框架选型 | 用途 | 工具 | 说明 | | --- | --- | --- | | **单元测试框架** | Vitest | Vite 原生,极速执行 | | **组件测试** | @testing-library/react | 用户行为驱动 | | **DOM 断言** | @testing-library/jest-dom | 扩展匹配器 | | **Mock 服务** | MSW (Mock Service Worker) | API 拦截 | | **E2E 测试** | Playwright | 跨浏览器 | | **视觉回归** | Percy / Chromatic | 截图对比 | | **覆盖率** | @vitest/coverage-v8 | 覆盖率报告 | ### 5.2 目录结构 ``` frontend/ ├── src/ │ ├── components/ │ │ ├── Button/ │ │ │ ├── Button.tsx │ │ │ ├── Button.test.tsx # 组件测试 │ │ │ └── Button.stories.tsx # Storybook (可选) │ │ └── ... │ │ │ ├── hooks/ │ │ ├── useAuth.ts │ │ ├── useAuth.test.ts # Hook 测试 │ │ └── ... │ │ │ ├── services/ │ │ ├── api.ts │ │ ├── api.test.ts # 服务测试 │ │ └── ... │ │ │ ├── store/ │ │ ├── auth.ts │ │ ├── auth.test.ts # 状态测试 │ │ └── ... │ │ │ └── lib/ │ ├── utils.ts │ ├── utils.test.ts # 工具函数测试 │ └── ... │ ├── tests/ │ ├── setup.ts # 测试全局配置 │ ├── mocks/ │ │ ├── handlers.ts # MSW 处理器 │ │ └── server.ts # MSW 服务器 │ │ │ ├── integration/ # 集成测试 │ │ ├── BriefUpload.test.tsx │ │ ├── VideoUpload.test.tsx │ │ └── ReviewDashboard.test.tsx │ │ │ └── e2e/ # Playwright E2E │ ├── creator-flow.spec.ts │ ├── agency-flow.spec.ts │ └── brand-flow.spec.ts │ ├── vitest.config.ts # Vitest 配置 ├── playwright.config.ts # Playwright 配置 └── package.json ``` ### 5.3 单元测试示例 #### 5.3.1 工具函数测试 ```typescript // src/lib/utils.test.ts import { describe, it, expect } from 'vitest' import { formatDuration, formatTimestamp, truncateText, validateVideoFile } from './utils' describe('formatDuration', () => { it('格式化秒数为 mm:ss', () => { expect(formatDuration(65)).toBe('01:05') expect(formatDuration(3661)).toBe('61:01') expect(formatDuration(0)).toBe('00:00') }) it('处理负数', () => { expect(formatDuration(-10)).toBe('00:00') }) }) describe('formatTimestamp', () => { it('格式化毫秒为 HH:MM:SS.mmm', () => { expect(formatTimestamp(1500)).toBe('00:00:01.500') expect(formatTimestamp(3661500)).toBe('01:01:01.500') }) }) describe('validateVideoFile', () => { it('接受有效的 MP4 文件', () => { const file = new File([''], 'test.mp4', { type: 'video/mp4' }) Object.defineProperty(file, 'size', { value: 50 * 1024 * 1024 }) // 50MB const result = validateVideoFile(file) expect(result.valid).toBe(true) }) it('拒绝超过 100MB 的文件', () => { const file = new File([''], 'test.mp4', { type: 'video/mp4' }) Object.defineProperty(file, 'size', { value: 150 * 1024 * 1024 }) // 150MB const result = validateVideoFile(file) expect(result.valid).toBe(false) expect(result.error).toContain('100MB') }) it('拒绝非视频格式', () => { const file = new File([''], 'test.pdf', { type: 'application/pdf' }) const result = validateVideoFile(file) expect(result.valid).toBe(false) expect(result.error).toContain('格式') }) }) ``` #### 5.3.2 React Hook 测试 ```typescript // src/hooks/useAuth.test.ts import { renderHook, act, waitFor } from '@testing-library/react' import { describe, it, expect, beforeEach, vi } from 'vitest' import { useAuth } from './useAuth' import { server } from '../tests/mocks/server' import { rest } from 'msw' describe('useAuth', () => { beforeEach(() => { localStorage.clear() }) it('初始状态为未登录', () => { const { result } = renderHook(() => useAuth()) expect(result.current.isAuthenticated).toBe(false) expect(result.current.user).toBeNull() }) it('登录成功后更新状态', async () => { const { result } = renderHook(() => useAuth()) await act(async () => { await result.current.login('test@example.com', 'password') }) await waitFor(() => { expect(result.current.isAuthenticated).toBe(true) expect(result.current.user?.email).toBe('test@example.com') }) }) it('登录失败时抛出错误', async () => { // 模拟 API 返回错误 server.use( rest.post('/api/auth/login', (req, res, ctx) => { return res(ctx.status(401), ctx.json({ error: '密码错误' })) }) ) const { result } = renderHook(() => useAuth()) await expect( act(async () => { await result.current.login('test@example.com', 'wrong') }) ).rejects.toThrow('密码错误') }) it('登出后清除状态', async () => { const { result } = renderHook(() => useAuth()) // 先登录 await act(async () => { await result.current.login('test@example.com', 'password') }) // 再登出 act(() => { result.current.logout() }) expect(result.current.isAuthenticated).toBe(false) expect(result.current.user).toBeNull() }) }) ``` #### 5.3.3 Zustand 状态测试 ```typescript // src/store/upload.test.ts import { describe, it, expect, beforeEach } from 'vitest' import { useUploadStore } from './upload' describe('useUploadStore', () => { beforeEach(() => { // 重置 store useUploadStore.setState({ files: [], uploadProgress: {}, isUploading: false, }) }) it('添加文件到上传队列', () => { const file = new File(['test'], 'test.mp4', { type: 'video/mp4' }) useUploadStore.getState().addFile(file) expect(useUploadStore.getState().files).toHaveLength(1) expect(useUploadStore.getState().files[0].name).toBe('test.mp4') }) it('更新上传进度', () => { const fileId = 'file-123' useUploadStore.getState().updateProgress(fileId, 50) expect(useUploadStore.getState().uploadProgress[fileId]).toBe(50) }) it('移除已完成的文件', () => { const file = new File(['test'], 'test.mp4', { type: 'video/mp4' }) useUploadStore.getState().addFile(file) const fileId = useUploadStore.getState().files[0].id useUploadStore.getState().removeFile(fileId) expect(useUploadStore.getState().files).toHaveLength(0) }) }) ``` #### 5.3.4 组件测试 ```typescript // src/components/video/VideoUpload.test.tsx import { render, screen, fireEvent, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { describe, it, expect, vi } from 'vitest' import { VideoUpload } from './VideoUpload' describe('VideoUpload', () => { it('渲染上传区域', () => { render() expect(screen.getByText(/拖拽视频到此处/)).toBeInTheDocument() expect(screen.getByText(/支持 MP4、MOV 格式/)).toBeInTheDocument() }) it('拖拽文件触发上传', async () => { const onUpload = vi.fn() render() const dropzone = screen.getByTestId('dropzone') const file = new File(['video content'], 'test.mp4', { type: 'video/mp4' }) fireEvent.drop(dropzone, { dataTransfer: { files: [file] } }) await waitFor(() => { expect(onUpload).toHaveBeenCalledWith(file) }) }) it('拒绝超大文件并显示错误', async () => { const onUpload = vi.fn() render() const file = new File([''], 'large.mp4', { type: 'video/mp4' }) Object.defineProperty(file, 'size', { value: 150 * 1024 * 1024 }) const dropzone = screen.getByTestId('dropzone') fireEvent.drop(dropzone, { dataTransfer: { files: [file] } }) await waitFor(() => { expect(screen.getByText(/文件大小不能超过 100MB/)).toBeInTheDocument() expect(onUpload).not.toHaveBeenCalled() }) }) it('显示上传进度', async () => { render() expect(screen.getByRole('progressbar')).toHaveAttribute('aria-valuenow', '45') expect(screen.getByText('45%')).toBeInTheDocument() }) }) ``` ### 5.4 MSW Mock 服务配置 ```typescript // tests/mocks/handlers.ts import { rest } from 'msw' export const handlers = [ // 认证 API rest.post('/api/auth/login', async (req, res, ctx) => { const { email, password } = await req.json() if (password === 'password') { return res(ctx.json({ access_token: 'mock-token', user: { id: '1', email, role: 'agency' } })) } return res(ctx.status(401), ctx.json({ error: '密码错误' })) }), // Brief API rest.get('/api/v1/briefs/:id', (req, res, ctx) => { return res(ctx.json({ id: req.params.id, selling_points: ['24小时持妆', '天然成分'], prohibited_words: ['药用', '治疗'], status: 'parsed' })) }), // 视频上传 API rest.post('/api/v1/videos/upload', async (req, res, ctx) => { return res(ctx.json({ task_id: 'mock-task-id', status: 'processing' })) }), // WebSocket 模拟 // 注意:MSW 不支持 WebSocket,需要单独的 mock ] ``` ```typescript // tests/mocks/server.ts import { setupServer } from 'msw/node' import { handlers } from './handlers' export const server = setupServer(...handlers) ``` ```typescript // tests/setup.ts import { beforeAll, afterEach, afterAll } from 'vitest' import { server } from './mocks/server' import '@testing-library/jest-dom' beforeAll(() => server.listen()) afterEach(() => server.resetHandlers()) afterAll(() => server.close()) ``` ### 5.5 前端测试自动化方案总结 | 测试类型 | 工具 | 覆盖范围 | 执行时机 | | --- | --- | --- | --- | | **单元测试** | Vitest | 工具函数、Hooks、Store | 每次提交 | | **组件测试** | Vitest + RTL | UI 组件行为 | 每次提交 | | **集成测试** | Vitest + MSW | 页面级交互 | 每次 PR | | **E2E 测试** | Playwright | 完整用户流程 | 每日/发布前 | | **视觉回归** | Percy/Chromatic | UI 外观变化 | 每次 PR | | **兼容性测试** | BrowserStack | 跨浏览器/设备 | 发布前 | --- ## 6. AI 模型测试策略 ### 6.1 AI 测试的特殊性 AI 模型测试与传统单元测试有本质区别: | 维度 | 传统测试 | AI 模型测试 | | --- | --- | --- | | **输出确定性** | 确定性输出 | 概率性输出 | | **验证方式** | 精确匹配 | 阈值验证 | | **测试数据** | 少量手工构造 | 大规模标注集 | | **回归检测** | 断言失败 | 指标下降 | | **维护成本** | 低 | 需持续更新 | ### 6.2 AI 测试分层 ``` ┌────────────────────────────────────────────────────────────────┐ │ AI 模型测试分层 │ ├────────────────────────────────────────────────────────────────┤ │ │ │ 【第1层:接口契约测试】 │ │ • 输入格式验证 │ │ • 输出结构验证 │ │ • 错误处理验证 │ │ → 使用 pytest + JSON Schema 验证 │ │ │ │ 【第2层:功能正确性测试】 │ │ • 标注测试集验证 │ │ • 边界情况覆盖 │ │ • 阈值达标检查 │ │ → 使用标注数据 + 指标计算 │ │ │ │ 【第3层:回归测试】 │ │ • 模型更新后的指标对比 │ │ • Prompt 修改后的行为验证 │ │ • 新增 Case 的持续覆盖 │ │ → 使用 MLflow + 版本对比 │ │ │ │ 【第4层:对抗测试】 │ │ • 边缘输入 (长文本、特殊字符、空输入) │ │ • 对抗样本 (刻意绕过检测) │ │ • 压力测试 (高并发、大文件) │ │ → 使用 fuzzing + 人工设计 │ │ │ └────────────────────────────────────────────────────────────────┘ ``` ### 6.3 各 AI 模块测试策略 #### 6.3.1 ASR 语音识别测试 ```python # tests/ai/test_asr.py import pytest from app.ai.asr import ASRService class TestASRService: """ASR 语音识别测试""" @pytest.fixture def asr(self): return ASRService() @pytest.fixture def test_audio_samples(self): """加载标注测试集""" return load_labeled_dataset("tests/fixtures/asr_samples/") def test_output_format(self, asr): """验证输出格式契约""" result = asr.transcribe("tests/fixtures/sample.wav") # 验证必需字段 assert "text" in result assert "timestamps" in result assert isinstance(result["timestamps"], list) # 验证时间戳格式 for ts in result["timestamps"]: assert "start" in ts assert "end" in ts assert "text" in ts assert ts["end"] >= ts["start"] def test_word_error_rate(self, asr, test_audio_samples): """验证字错率 ≤ 10%""" total_errors = 0 total_words = 0 for sample in test_audio_samples: result = asr.transcribe(sample["audio_path"]) wer = calculate_wer(result["text"], sample["ground_truth"]) total_errors += wer * len(sample["ground_truth"].split()) total_words += len(sample["ground_truth"].split()) overall_wer = total_errors / total_words assert overall_wer <= 0.10, f"WER {overall_wer:.2%} 超过阈值 10%" def test_timestamp_accuracy(self, asr, test_audio_samples): """验证时间戳准确性""" for sample in test_audio_samples: result = asr.transcribe(sample["audio_path"]) # 验证起止时间与音频时长匹配 audio_duration = get_audio_duration(sample["audio_path"]) last_timestamp = result["timestamps"][-1]["end"] # 允许 500ms 误差 assert abs(last_timestamp - audio_duration * 1000) <= 500 ``` #### 6.3.2 违禁词检测测试 ```python # tests/ai/test_prohibited_words.py import pytest from app.ai.nlp import ProhibitedWordDetector class TestProhibitedWordDetector: """违禁词检测测试""" @pytest.fixture def detector(self): return ProhibitedWordDetector() @pytest.fixture def labeled_dataset(self): """ 标注数据集格式: { "text": "这是全网销量第一的产品", "context": "advertisement", "expected_violations": ["全网第一"], "should_block": true } """ return load_labeled_dataset("tests/fixtures/prohibited_words/") def test_recall_rate(self, detector, labeled_dataset): """验证召回率 ≥ 95%""" true_positives = 0 false_negatives = 0 for sample in labeled_dataset: if not sample["expected_violations"]: continue result = detector.detect(sample["text"], sample["context"]) detected = set(result.violations) expected = set(sample["expected_violations"]) true_positives += len(detected & expected) false_negatives += len(expected - detected) recall = true_positives / (true_positives + false_negatives) assert recall >= 0.95, f"召回率 {recall:.2%} 低于阈值 95%" def test_false_positive_rate(self, detector, labeled_dataset): """验证误报率 ≤ 5%""" false_positives = 0 true_negatives = 0 # 只测试不应有违规的样本 negative_samples = [ s for s in labeled_dataset if not s["expected_violations"] ] for sample in negative_samples: result = detector.detect(sample["text"], sample["context"]) if result.violations: false_positives += 1 else: true_negatives += 1 fpr = false_positives / (false_positives + true_negatives) assert fpr <= 0.05, f"误报率 {fpr:.2%} 超过阈值 5%" def test_context_awareness(self, detector): """验证语境感知能力""" text = "这是我最开心的一天" # 广告语境 - 不应误报 result_ad = detector.detect(text, context="advertisement") assert len(result_ad.violations) == 0, "日常用语在广告语境误报" # 日常语境 - 不应误报 result_daily = detector.detect(text, context="daily_conversation") assert len(result_daily.violations) == 0, "日常用语误报" ``` #### 6.3.3 Logo 向量检索测试 ```python # tests/ai/test_logo_detection.py import pytest from app.ai.cv import LogoDetector class TestLogoDetector: """Logo 检测测试""" @pytest.fixture def detector(self): return LogoDetector() @pytest.fixture def logo_test_set(self): """ 测试集包含: - 200+ 竞品 Logo 图片 - 各种遮挡、模糊、旋转场景 - 负样本(无 Logo 的图片) """ return load_labeled_dataset("tests/fixtures/logos/") def test_f1_score(self, detector, logo_test_set): """验证 F1 ≥ 0.85""" predictions = [] ground_truths = [] for sample in logo_test_set: result = detector.detect(sample["image_path"]) predictions.append(result.detected_logos) ground_truths.append(sample["ground_truth_logos"]) f1 = calculate_f1(predictions, ground_truths) assert f1 >= 0.85, f"F1 {f1:.2f} 低于阈值 0.85" def test_partial_occlusion(self, detector, logo_test_set): """验证 30% 遮挡场景下的检测能力""" occluded_samples = [ s for s in logo_test_set if s.get("occlusion_rate", 0) >= 0.3 ] correct = 0 for sample in occluded_samples: result = detector.detect(sample["image_path"]) if sample["ground_truth_logos"] == result.detected_logos: correct += 1 accuracy = correct / len(occluded_samples) # 遮挡场景允许稍低的准确率 assert accuracy >= 0.75, f"遮挡场景准确率 {accuracy:.2%} 过低" def test_new_logo_instant_detection(self, detector): """验证新 Logo 上传后即刻生效""" # 上传新 Logo new_logo_path = "tests/fixtures/new_competitor_logo.png" detector.add_logo(new_logo_path, brand="New Competitor") # 立即测试检测 test_frame = "tests/fixtures/frame_with_new_logo.jpg" result = detector.detect(test_frame) assert "New Competitor" in result.detected_logos ``` ### 6.4 LLM Prompt 测试 ```python # tests/ai/test_llm.py import pytest from app.ai.llm import LLMService class TestLLMPrompts: """LLM Prompt 测试""" @pytest.fixture def llm(self): return LLMService() @pytest.fixture def few_shot_examples(self): """Few-shot 示例集""" return load_few_shot_examples("tests/fixtures/llm_examples/") def test_brief_parsing_output_format(self, llm): """验证 Brief 解析输出格式""" brief_content = """ 产品卖点:24小时持妆 禁止使用:药用、治疗 """ result = llm.parse_brief(brief_content) # 验证输出结构 assert "selling_points" in result assert "prohibited_words" in result assert isinstance(result["selling_points"], list) assert isinstance(result["prohibited_words"], list) def test_context_understanding(self, llm, few_shot_examples): """验证语境理解能力""" context_examples = [ e for e in few_shot_examples if e["type"] == "context_understanding" ] correct = 0 for example in context_examples: result = llm.classify_context(example["text"]) if result["context"] == example["expected_context"]: correct += 1 accuracy = correct / len(context_examples) assert accuracy >= 0.90, f"语境理解准确率 {accuracy:.2%} 过低" def test_sentiment_analysis(self, llm): """验证舆情风险检测""" test_cases = [ {"text": "这个产品太油腻了", "expected_risk": "greasy"}, {"text": "正常的产品介绍", "expected_risk": None}, {"text": "男人就该这样", "expected_risk": "gender_bias"}, ] for case in test_cases: result = llm.analyze_sentiment(case["text"]) if case["expected_risk"]: assert result.risk_type == case["expected_risk"] else: assert result.risk_type is None ``` ### 6.5 AI 测试数据集管理 ``` tests/fixtures/ ├── asr_samples/ # ASR 测试集 │ ├── manifest.json # 数据清单 │ ├── audio/ │ │ ├── sample_001.wav │ │ └── ... │ └── transcripts/ │ ├── sample_001.json # 标注结果 │ └── ... │ ├── prohibited_words/ # 违禁词测试集 │ ├── positive_samples.json # 应检出样本 │ ├── negative_samples.json # 不应检出样本 │ └── context_samples.json # 语境测试样本 │ ├── logos/ # Logo 测试集 │ ├── manifest.json │ ├── images/ │ │ ├── logo_001.jpg │ │ └── ... │ └── annotations/ │ ├── logo_001.json │ └── ... │ ├── llm_examples/ # LLM 测试集 │ ├── brief_parsing.json │ ├── context_understanding.json │ └── sentiment_analysis.json │ └── README.md # 测试集说明文档 ``` --- ## 7. 端到端测试策略 ### 7.1 E2E 测试框架 | 工具 | 用途 | | --- | --- | | **Playwright** | 跨浏览器 E2E 测试 | | **@playwright/test** | 测试运行器 | | **playwright-report** | 测试报告 | | **BrowserStack** | 真机云测试 | ### 7.2 核心用户流程测试 ```typescript // tests/e2e/creator-flow.spec.ts import { test, expect } from '@playwright/test' test.describe('达人端完整流程', () => { test.beforeEach(async ({ page }) => { // 登录达人账号 await page.goto('/auth/login') await page.fill('[name="email"]', 'creator@test.com') await page.fill('[name="password"]', 'password') await page.click('button[type="submit"]') await expect(page).toHaveURL('/creator/tasks') }) test('上传视频 → 等待审核 → 查看结果', async ({ page }) => { // 1. 进入上传页面 await page.click('text=上传') await expect(page).toHaveURL('/creator/upload') // 2. 上传视频 const fileInput = page.locator('input[type="file"]') await fileInput.setInputFiles('tests/fixtures/test_video.mp4') // 3. 等待上传完成 await expect(page.locator('.upload-progress')).toHaveText(/100%/) // 4. 等待审核完成(可能需要等待) await page.click('button:has-text("提交审核")') // 5. 验证进入审核中状态 await expect(page.locator('.audit-status')).toHaveText(/审核中/) // 6. 等待审核完成(最多 5 分钟) await expect(page.locator('.audit-status')).toHaveText( /已通过|需修改/, { timeout: 300000 } ) // 7. 验证结果页面 await page.click('text=查看结果') await expect(page.locator('.result-banner')).toBeVisible() }) test('申诉流程', async ({ page }) => { // 假设有一个需修改的任务 await page.goto('/creator/tasks?status=needs_revision') await page.click('.task-card >> nth=0') // 1. 点击申诉按钮 await page.click('button:has-text("申诉")') // 2. 填写申诉理由 await page.fill('textarea[name="reason"]', '这不是广告用语,是日常表达') // 3. 提交申诉 await page.click('button:has-text("提交申诉")') // 4. 验证申诉成功 await expect(page.locator('.toast')).toHaveText(/申诉已提交/) }) }) ``` ```typescript // tests/e2e/agency-flow.spec.ts import { test, expect } from '@playwright/test' test.describe('代理商端完整流程', () => { test.beforeEach(async ({ page }) => { // 登录代理商账号 await page.goto('/auth/login') await page.fill('[name="email"]', 'agency@test.com') await page.fill('[name="password"]', 'password') await page.click('button[type="submit"]') await expect(page).toHaveURL('/agency/dashboard') }) test('配置 Brief → 审核视频 → 通过', async ({ page }) => { // 1. 上传 Brief await page.click('text=Brief 管理') await page.click('button:has-text("上传 Brief")') const fileInput = page.locator('input[type="file"]') await fileInput.setInputFiles('tests/fixtures/test_brief.pdf') // 2. 等待解析完成 await expect(page.locator('.parsing-status')).toHaveText(/解析完成/, { timeout: 60000 }) // 3. 确认规则 await page.click('button:has-text("确认规则")') // 4. 进入审核台 await page.click('text=审核台') await page.click('.pending-task >> nth=0') // 5. 查看视频和检查单 await expect(page.locator('.video-player')).toBeVisible() await expect(page.locator('.checklist')).toBeVisible() // 6. 通过审核 await page.click('button:has-text("通过")') await page.click('button:has-text("确认")') // 7. 验证状态更新 await expect(page.locator('.task-status')).toHaveText(/已通过/) }) test('驳回视频', async ({ page }) => { await page.goto('/agency/review') await page.click('.pending-task >> nth=0') // 勾选问题 await page.check('input[name="issue_0"]') await page.check('input[name="issue_1"]') // 驳回 await page.click('button:has-text("驳回")') await page.click('button:has-text("确认")') await expect(page.locator('.task-status')).toHaveText(/已驳回/) }) }) ``` ### 7.3 移动端 E2E 测试 ```typescript // tests/e2e/mobile-creator.spec.ts import { test, expect, devices } from '@playwright/test' test.use({ ...devices['iPhone 13'], }) test.describe('达人端 H5 移动端测试', () => { test('移动端上传视频', async ({ page }) => { await page.goto('/creator/upload') // 验证移动端布局 await expect(page.locator('.bottom-nav')).toBeVisible() // 验证防锁屏提示 await expect(page.locator('.wakelock-hint')).toBeVisible() // 模拟上传 const fileInput = page.locator('input[type="file"]') await fileInput.setInputFiles('tests/fixtures/test_video.mp4') // 验证进度显示 await expect(page.locator('.circular-progress')).toBeVisible() }) }) ``` ### 7.4 Playwright 配置 ```typescript // playwright.config.ts import { defineConfig, devices } from '@playwright/test' export default defineConfig({ testDir: './tests/e2e', timeout: 60000, retries: 2, workers: 4, reporter: [ ['html', { outputFolder: 'playwright-report' }], ['junit', { outputFile: 'test-results/junit.xml' }], ], use: { baseURL: process.env.TEST_BASE_URL || 'http://localhost:3000', trace: 'on-first-retry', screenshot: 'only-on-failure', video: 'on-first-retry', }, projects: [ // 桌面浏览器 { name: 'chromium', use: { ...devices['Desktop Chrome'] } }, { name: 'firefox', use: { ...devices['Desktop Firefox'] } }, { name: 'webkit', use: { ...devices['Desktop Safari'] } }, // 移动端 { name: 'mobile-chrome', use: { ...devices['Pixel 5'] } }, { name: 'mobile-safari', use: { ...devices['iPhone 13'] } }, ], // 本地开发服务器 webServer: { command: 'npm run dev', url: 'http://localhost:3000', reuseExistingServer: !process.env.CI, }, }) ``` --- ## 8. 实施路线图 ### 8.1 分阶段实施计划 ``` ┌──────────────────────────────────────────────────────────────────────┐ │ TDD 实施路线图 (11 周) │ ├──────────────────────────────────────────────────────────────────────┤ │ │ │ Phase 0: 基础建设 (Week 0, 并行进行) │ │ ┌────────────────────────────────────────────────────────────────┐ │ │ │ • 搭建 pytest/Vitest 测试框架 │ │ │ │ • 配置 CI/CD 测试流水线 │ │ │ │ • 建立代码覆盖率门禁 │ │ │ │ • 准备 AI 测试数据集 (初始 100+ 样本) │ │ │ │ • 团队 TDD 培训 │ │ │ └────────────────────────────────────────────────────────────────┘ │ │ ↓ │ │ Phase 1: 基础设施 + Brief 引擎 (Week 1-2) │ │ ┌────────────────────────────────────────────────────────────────┐ │ │ │ 后端 TDD (100% 覆盖): │ │ │ │ • 数据模型测试 → 数据模型实现 │ │ │ │ • 验证器测试 → 验证器实现 │ │ │ │ • 规则引擎测试 → 规则引擎实现 │ │ │ │ • Brief 解析测试 → Brief 解析实现 │ │ │ │ │ │ │ │ 前端 TDD (基础组件): │ │ │ │ • 工具函数测试 → 工具函数实现 │ │ │ │ • 基础组件测试 → 基础组件实现 │ │ │ │ │ │ │ │ API Mock 服务搭建 │ │ │ └────────────────────────────────────────────────────────────────┘ │ │ ↓ │ │ Phase 2: 核心 AI 流水线 (Week 3-6) │ │ ┌────────────────────────────────────────────────────────────────┐ │ │ │ AI 模型测试 (标注集验证): │ │ │ │ • 建立 ASR/OCR/CV 测试集 (≥500 样本) │ │ │ │ • 接口契约测试 → AI 服务封装 │ │ │ │ • 阈值验证测试 → 模型调优 │ │ │ │ │ │ │ │ 核心算法 TDD: │ │ │ │ • 时间戳对齐测试 → 对齐算法实现 │ │ │ │ • 多模态融合测试 → 融合逻辑实现 │ │ │ │ │ │ │ │ 集成测试: │ │ │ │ • Celery 任务测试 │ │ │ │ • WebSocket 推送测试 │ │ │ └────────────────────────────────────────────────────────────────┘ │ │ ↓ │ │ Phase 3: 界面开发 (Week 7-9) │ │ ┌────────────────────────────────────────────────────────────────┐ │ │ │ 前端组件 TDD: │ │ │ │ • 组件测试 → 组件实现 │ │ │ │ • Hook 测试 → Hook 实现 │ │ │ │ • Store 测试 → Store 实现 │ │ │ │ │ │ │ │ 页面集成测试: │ │ │ │ • MSW Mock + 页面交互测试 │ │ │ │ │ │ │ │ E2E 测试骨架: │ │ │ │ • 核心用户流程 E2E (Playwright) │ │ │ └────────────────────────────────────────────────────────────────┘ │ │ ↓ │ │ Phase 4: 联调与验收 (Week 10-11) │ │ ┌────────────────────────────────────────────────────────────────┐ │ │ │ 测试完善: │ │ │ │ • E2E 测试补全 │ │ │ │ • 性能测试 (Locust) │ │ │ │ • 兼容性测试 (BrowserStack) │ │ │ │ │ │ │ │ AI 模型验收: │ │ │ │ • 完整测试集运行 │ │ │ │ • 指标达标验证 │ │ │ │ │ │ │ │ 回归测试: │ │ │ │ • 全量回归 │ │ │ │ • 冒烟测试自动化 │ │ │ └────────────────────────────────────────────────────────────────┘ │ │ │ └──────────────────────────────────────────────────────────────────────┘ ``` ### 8.2 每周测试交付物 | 周次 | 测试交付物 | 覆盖率目标 | | --- | --- | --- | | Week 1 | 后端框架测试、数据模型测试 | 后端 80% | | Week 2 | Brief 解析测试、规则引擎测试 | 后端 80% | | Week 3 | ASR/OCR 接口测试 | AI 模块 60% | | Week 4 | CV 检测测试、向量检索测试 | AI 模块 70% | | Week 5 | 时间戳对齐测试、多模态融合测试 | AI 模块 80% | | Week 6 | Celery 任务测试、WebSocket 测试 | 后端 85% | | Week 7 | 前端工具函数测试、Hook 测试 | 前端 60% | | Week 8 | 前端组件测试、Store 测试 | 前端 70% | | Week 9 | 页面集成测试、E2E 骨架 | 前端 75% | | Week 10 | E2E 补全、性能测试 | E2E 核心路径 100% | | Week 11 | 兼容性测试、全量回归 | 整体 75% | --- ## 9. 测试覆盖率目标 ### 9.1 覆盖率门禁 | 层级 | 目标覆盖率 | 门禁策略 | | --- | --- | --- | | **后端单元测试** | ≥ 80% | PR 阻断 | | **前端单元测试** | ≥ 70% | PR 阻断 | | **AI 模块测试** | ≥ 70% | PR 阻断 | | **集成测试** | ≥ 60% | PR 警告 | | **E2E 测试** | 核心路径 100% | 发布阻断 | ### 9.2 覆盖率例外 以下代码可豁免覆盖率要求: | 代码类型 | 原因 | | --- | --- | | 第三方 SDK 封装 | 信任上游 | | 环境配置代码 | 运行时验证 | | 日志/监控代码 | 非核心逻辑 | | 迁移脚本 | 一次性执行 | ### 9.3 覆盖率报告 ```yaml # .github/workflows/test.yml (覆盖率报告部分) - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: files: ./coverage/coverage.xml fail_ci_if_error: true - name: Coverage Gate run: | COVERAGE=$(cat coverage/coverage.txt | grep "TOTAL" | awk '{print $4}' | tr -d '%') if [ "$COVERAGE" -lt "75" ]; then echo "Coverage $COVERAGE% is below threshold 75%" exit 1 fi ``` --- ## 10. 工具链配置 ### 10.1 后端工具链 ```toml # pyproject.toml [tool.pytest.ini_options] testpaths = ["tests"] python_files = ["test_*.py"] python_functions = ["test_*"] addopts = [ "-v", "--tb=short", "--strict-markers", "-ra", "--cov=app", "--cov-report=xml", "--cov-report=html", "--cov-fail-under=75", ] asyncio_mode = "auto" markers = [ "slow: 标记慢速测试", "integration: 集成测试", "ai: AI 模型测试", ] [tool.coverage.run] branch = true source = ["app"] omit = [ "*/migrations/*", "*/tests/*", "*/__init__.py", ] [tool.coverage.report] exclude_lines = [ "pragma: no cover", "def __repr__", "raise NotImplementedError", "if TYPE_CHECKING:", ] ``` ### 10.2 前端工具链 ```typescript // vitest.config.ts import { defineConfig } from 'vitest/config' import react from '@vitejs/plugin-react' import path from 'path' export default defineConfig({ plugins: [react()], test: { globals: true, environment: 'jsdom', setupFiles: ['./tests/setup.ts'], include: ['**/*.test.{ts,tsx}'], coverage: { provider: 'v8', reporter: ['text', 'json', 'html'], exclude: [ 'node_modules/', 'tests/', '**/*.d.ts', '**/*.config.*', ], thresholds: { lines: 70, functions: 70, branches: 70, statements: 70, }, }, }, resolve: { alias: { '@': path.resolve(__dirname, './src'), }, }, }) ``` ### 10.3 CI/CD 配置 ```yaml # .github/workflows/test.yml name: Test Suite on: push: branches: [main, develop] pull_request: branches: [main, develop] jobs: backend-test: runs-on: ubuntu-latest services: postgres: image: postgres:15 env: POSTGRES_PASSWORD: test ports: - 5432:5432 redis: image: redis:7 ports: - 6379:6379 steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.11' cache: 'pip' - name: Install dependencies run: | cd backend pip install -r requirements.txt pip install -r requirements-dev.txt - name: Run linting run: | cd backend ruff check . mypy app - name: Run tests run: | cd backend pytest --cov --cov-report=xml env: DATABASE_URL: postgresql://postgres:test@localhost/test REDIS_URL: redis://localhost:6379 - name: Upload coverage uses: codecov/codecov-action@v3 with: files: backend/coverage.xml frontend-test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm' cache-dependency-path: frontend/package-lock.json - name: Install dependencies run: | cd frontend npm ci - name: Run linting run: | cd frontend npm run lint - name: Run tests run: | cd frontend npm run test:coverage - name: Upload coverage uses: codecov/codecov-action@v3 with: files: frontend/coverage/coverage-final.json e2e-test: runs-on: ubuntu-latest needs: [backend-test, frontend-test] steps: - uses: actions/checkout@v4 - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: '20' - name: Install Playwright run: | cd frontend npm ci npx playwright install --with-deps - name: Run E2E tests run: | cd frontend npm run test:e2e - name: Upload Playwright report uses: actions/upload-artifact@v3 if: failure() with: name: playwright-report path: frontend/playwright-report/ ``` --- ## 11. 团队规范与培训 ### 11.1 TDD 工作流规范 ``` ┌────────────────────────────────────────────────────────────────────┐ │ TDD 红-绿-重构循环 │ ├────────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────┐ │ │ │ 🔴 RED │ 1. 编写一个失败的测试 │ │ │ (失败) │ • 测试必须能运行 │ │ │ │ • 测试必须失败 │ │ └──────┬──────┘ • 失败原因是功能未实现 │ │ │ │ │ ▼ │ │ ┌─────────────┐ │ │ │ 🟢 GREEN │ 2. 编写最少的代码让测试通过 │ │ │ (通过) │ • 不要过度设计 │ │ │ │ • 只写足够通过测试的代码 │ │ └──────┬──────┘ • 可以"作弊"(硬编码) │ │ │ │ │ ▼ │ │ ┌─────────────┐ │ │ │ 🔄 REFACTOR │ 3. 重构代码 │ │ │ (重构) │ • 移除重复 │ │ │ │ • 改善设计 │ │ └──────┬──────┘ • 测试仍然通过 │ │ │ │ │ └──────────────────────────────────────────────────────► │ │ 循环 │ │ │ └────────────────────────────────────────────────────────────────────┘ ``` ### 11.2 测试命名规范 ```python # Python (pytest) class TestCalculator: def test_add_two_positive_numbers_returns_sum(self): """测试两个正数相加返回正确的和""" pass def test_divide_by_zero_raises_error(self): """测试除以零抛出错误""" pass ``` ```typescript // TypeScript (Vitest) describe('Calculator', () => { it('should return sum when adding two positive numbers', () => { // ... }) it('should throw error when dividing by zero', () => { // ... }) }) ``` ### 11.3 测试文件组织规范 | 规则 | 说明 | | --- | --- | | 测试文件与源文件同目录 | `utils.ts` → `utils.test.ts` | | 测试目录 `__tests__` | 复杂模块可用目录 | | 命名后缀 `.test.ts` / `_test.py` | 便于识别和自动发现 | | 每个测试文件只测一个模块 | 职责单一 | ### 11.4 团队培训计划 | 阶段 | 时长 | 内容 | 交付物 | | --- | --- | --- | --- | | **TDD 基础** | 2h | TDD 概念、红绿重构循环 | 培训 PPT | | **pytest 实战** | 2h | pytest 使用、fixture、参数化 | 示例代码 | | **Vitest 实战** | 2h | Vitest 使用、RTL、MSW | 示例代码 | | **AI 测试** | 2h | 标注集管理、阈值验证 | 测试模板 | | **代码审查** | 持续 | PR 中检查测试质量 | 审查清单 | ### 11.5 代码审查清单 ```markdown ## PR 测试审查清单 ### 必须项 - [ ] 新功能有对应的单元测试 - [ ] 测试覆盖了正常路径和异常路径 - [ ] 测试命名清晰,描述预期行为 - [ ] 测试独立运行,不依赖执行顺序 - [ ] 覆盖率不低于门禁阈值 ### 建议项 - [ ] 使用参数化测试覆盖多种输入 - [ ] Mock 外部依赖,避免测试不稳定 - [ ] 测试执行时间 < 1秒(单元测试) - [ ] 无硬编码的测试数据(使用 fixture) ### AI 模块特别项 - [ ] 有对应的标注测试集 - [ ] 验证了输出格式 - [ ] 验证了阈值指标 ``` --- ## 12. 风险与挑战 ### 12.1 风险矩阵 | 风险 | 可能性 | 影响 | 缓解措施 | | --- | --- | --- | --- | | **AI 模型幻觉** | 🔴 高 | 🔴 高 | 完整标注集 + 人工抽查 + 持续监控 | | **测试数据不足** | 🟡 中 | 🔴 高 | 持续收集真实数据 + 数据增强 | | **E2E 测试不稳定** | 🟡 中 | 🟡 中 | 重试机制 + 等待策略优化 | | **团队 TDD 经验不足** | 🟡 中 | 🟡 中 | 培训 + 结对编程 + 代码审查 | | **测试维护成本高** | 🟡 中 | 🟡 中 | 测试重构 + 共享 fixture | | **CI/CD 执行慢** | 🟢 低 | 🟡 中 | 并行执行 + 增量测试 | ### 12.2 AI 测试特殊挑战 | 挑战 | 应对策略 | | --- | --- | | **LLM 输出不确定性** | 验证结构而非精确内容 + 多次采样取共识 | | **Prompt 变更影响大** | 建立 Prompt 版本管理 + 回归测试 | | **标注成本高** | 优先覆盖高风险场景 + 主动学习采样 | | **模型更新回归** | 建立基线 + 自动化指标对比 | | **边缘情况难穷尽** | 对抗样本生成 + 持续收集 badcase | ### 12.3 TDD 常见误区 | 误区 | 正确做法 | | --- | --- | | 先写代码再补测试 | 严格遵循红-绿-重构 | | 追求 100% 覆盖率 | 关注有意义的测试 | | 测试实现细节 | 测试行为和结果 | | 过度 Mock | 只 Mock 真正的外部依赖 | | 测试代码不维护 | 测试代码同样需要重构 | --- ## 13. 附录 ### 13.1 相关文档 | 文档 | 说明 | | --- | --- | | tasks.md | 开发任务清单 | | DevelopmentPlan.md | 技术架构与开发计划 | | FeatureSummary.md | 功能清单与验收标准 | | User_Role_Interfaces.md | 用户角色与界面规范 | ### 13.2 参考资源 | 资源 | 链接 | | --- | --- | | pytest 官方文档 | https://docs.pytest.org/ | | Vitest 官方文档 | https://vitest.dev/ | | Testing Library | https://testing-library.com/ | | Playwright 官方文档 | https://playwright.dev/ | | MSW 官方文档 | https://mswjs.io/ | | TDD by Example (书籍) | Kent Beck | ### 13.3 术语表 | 术语 | 定义 | | --- | --- | | **TDD** | Test-Driven Development,测试驱动开发 | | **BDD** | Behavior-Driven Development,行为驱动开发 | | **SUT** | System Under Test,被测系统 | | **Fixture** | 测试固定装置,用于准备测试环境 | | **Mock** | 模拟对象,替代真实依赖 | | **Stub** | 存根,返回预设值的简化实现 | | **Coverage** | 代码覆盖率 | | **Regression** | 回归测试 | --- ## 14. 总结 ### 14.1 核心结论 1. **SmartAudit 项目高度适合实施 TDD** - 零代码起步,是最佳切入点 - 需求明确,验收标准量化 - 技术栈测试生态成熟 2. **采用分层混合 TDD 策略** - 业务逻辑:严格 TDD - AI 模型:标注集验证 - E2E:BDD + 自动化 3. **前端测试自动化方案** - 单元测试:Vitest + Testing Library - 组件测试:RTL + MSW - E2E 测试:Playwright - 视觉回归:Percy/Chromatic 4. **关键成功因素** - 测试框架在 Week 0 搭建完成 - CI/CD 门禁从第一行代码开始 - AI 测试集持续积累 - 团队培训与规范执行 ### 14.2 下一步行动 | 优先级 | 行动项 | 负责人 | 时间 | | --- | --- | --- | --- | | P0 | 创建 backend/tests/ 目录结构 | Backend Lead | Week 0 | | P0 | 配置 pytest + CI/CD | Backend Lead | Week 0 | | P0 | 创建 frontend/tests/ 目录结构 | Frontend Lead | Week 0 | | P0 | 配置 Vitest + Playwright | Frontend Lead | Week 0 | | P0 | 团队 TDD 培训 | Tech Lead | Week 0 | | P1 | 建立 AI 测试数据集框架 | AI Engineer | Week 1 | | P1 | 编写核心模块测试规范文档 | Tech Lead | Week 1 | --- **文档状态**:✅ 完成 **下次审阅**:开发启动后 2 周