# Audio CodeBuddy 测试体系设计文档 > **版本**: v1.0 > **作者**: 测试负责人 > **日期**: 2026-05-19 > **状态**: 正式发布 --- ## 目录 1. [现状评估](#一现状评估) 2. [测试体系架构](#二测试体系架构) 3. [单元测试设计](#三单元测试设计) 4. [集成测试设计](#四集成测试设计) 5. [端到端测试设计](#五端到端测试设计) 6. [测试基础设施](#六测试基础设施) 7. [CI/CD 流水线](#七cicd-流水线) 8. [质量门禁](#八质量门禁) 9. [分阶段实施路线图](#九分阶段实施路线图) 10. [附录](#十附录) --- ## 一、现状评估 ### 1.1 当前测试能力 | 维度 | 现状 | 评分 | |------|------|------| | 单元测试(后端) | ❌ 完全缺失 | 0/10 | | 单元测试(前端) | ❌ 完全缺失 | 0/10 | | 集成测试(API) | ⚠️ 有但不规范,断言过宽 | 3/10 | | E2E 测试(UI) | ⚠️ 有但流于表面,只测页面存在 | 4/10 | | 测试基础设施 | ✅ Playwright setup/teardown 设计良好 | 7/10 | | CI/CD 集成 | ⚠️ 不完善,依赖环境变量 | 4/10 | | **综合评分** | | **3.0/10** | ### 1.2 核心风险 | 风险等级 | 风险项 | 说明 | |----------|--------|------| | 🔴 致命 | **LangGraph 书籍生成无测试** | 97KB 的大控制器,12 个 AI 节点,0 测试覆盖 | | 🔴 致命 | **计费/订阅系统无测试** | 涉及金额计算,错误 = 直接经济损失 | | 🔴 致命 | **TTS Provider 熔断器无测试** | 生产环境 API 故障时熔断逻辑是否正确? | | 🟡 高危 | **支付回调无测试** | 支付宝/微信支付回调逻辑无验证 | | 🟡 高危 | **音频配额扣除无测试** | 按分钟计费,精度问题可能导致少扣或多扣 | | 🟢 中等 | **前端表单验证无测试** | 用户输入错误缺少保护 | | 🟢 中等 | **Prisma 数据操作无测试** | ORM 模型变更可能破坏查询 | --- ## 二、测试体系架构 ### 2.1 测试金字塔 ``` ┌─────────────────┐ │ E2E 测试 (5%) │ 10 条关键用户旅程 │ Playwright │ 完整业务流程验证 ┌┴─────────────────┴┐ │ 集成测试 (20%) │ 40+ 条 API 流程测试 │ Vitest + Supertest│ 模块间交互 + 数据库 ┌┴───────────────────┴┐ │ 组件测试 (15%) │ 30+ 条 Vue 组件测试 │ Vitest + vue-test │ 表单/交互/状态管理 ┌┴─────────────────────┴┐ │ 单元测试 (60%) │ 200+ 条纯函数/类测试 │ Vitest │ 核心逻辑全覆盖 └┴───────────────────────┘ ``` ### 2.2 测试分层定义 | 层级 | 工具 | 运行时间 | 运行频率 | 覆盖率目标 | |------|------|---------|---------|-----------| | **L0 - 单元测试** | Vitest | <30s | 每次 git push | 行 80% / 分支 70% | | **L1 - 组件测试** | Vitest + @vue/test-utils | <60s | 每次 git push | 关键组件 90% | | **L2 - 集成测试** | Vitest + Supertest | <120s | 每次 PR | 关键 API 80% | | **L3 - API E2E** | Playwright APIRequest | <180s | 每次 PR | 所有端点 | | **L4 - UI E2E** | Playwright Browser | <300s | 每次发版 / 每日 | 10条关键旅程 | ### 2.3 测试文件组织 ``` tests/ ├── unit/ # L0 单元测试 │ ├── server/ # 后端单元测试(镜像 src/modules/ 结构) │ │ ├── common/ │ │ │ ├── audio-calculator.test.ts │ │ │ └── quote-checker.test.ts │ │ ├── tts/ │ │ │ ├── provider-registry.test.ts │ │ │ ├── circuit-breaker.test.ts │ │ │ └── aliyun-provider.test.ts │ │ ├── subscription/ │ │ │ ├── quota-checker.test.ts │ │ │ └── plan-calculator.test.ts │ │ ├── payment/ │ │ │ └── order-service.test.ts │ │ ├── book-generator/ │ │ │ ├── state-machine.test.ts │ │ │ ├── work-nodes.test.ts │ │ │ └── audio-merger.test.ts │ │ ├── player/ │ │ │ └── progress-service.test.ts │ │ └── auth/ │ │ └── jwt-service.test.ts │ └── frontend/ # 前端单元测试 │ ├── utils/ │ │ ├── request.test.ts │ │ └── format.test.ts │ └── stores/ │ ├── tts-store.test.ts │ └── user-store.test.ts │ ├── component/ # L1 组件测试 │ ├── player/ │ │ └── AudioPlayer.test.ts │ ├── book-generator/ │ │ ├── CreateForm.test.ts │ │ └── ChapterList.test.ts │ └── common/ │ ├── PlayButton.test.ts │ └── AudioCard.test.ts │ ├── integration/ # L2 集成测试 │ ├── api/ │ │ ├── auth-flow.test.ts │ │ ├── book-generator-flow.test.ts │ │ ├── tts-flow.test.ts │ │ ├── payment-flow.test.ts │ │ ├── subscription-flow.test.ts │ │ └── publish-flow.test.ts │ └── db/ │ ├── prisma-crud.test.ts │ └── transaction.test.ts │ ├── e2e/ # L3-L4 E2E 测试 │ ├── api/ # L3 API E2E │ │ ├── health.spec.ts # 保留旧文件(改进版) │ │ └── api-crud.spec.ts # CRUD 完整测试 │ ├── ui/ # L4 UI E2E │ │ ├── critical-journeys/ # 关键旅程(新增) │ │ │ ├── journey-01-register-login.spec.ts │ │ │ ├── journey-02-free-tts.spec.ts │ │ │ ├── journey-03-book-generate.spec.ts │ │ │ ├── journey-04-subscribe-play.spec.ts │ │ │ └── journey-05-publish.spec.ts │ │ └── smoke/ # 保持旧结构 │ │ ├── health.spec.ts │ │ └── pages-load.spec.ts │ └── regression/ # 保留旧结构 │ ├── 01-auth.spec.ts │ ├── 02-tts.spec.ts │ └── ... │ └── helpers/ # 测试辅助 ├── fixtures/ # 测试夹具 │ ├── user-fixture.ts │ ├── book-fixture.ts │ └── audio-fixture.ts ├── mocks/ # Mock 工厂 │ ├── llm-mock.ts │ ├── tts-mock.ts │ └── payment-mock.ts └── db-helper.ts # 数据库测试助手 ``` --- ## 三、单元测试设计 ### 3.1 优先级矩阵 ``` 影响 × 复杂度 = 测试优先级 高影响 │ P2 │ P1(先测)│ │ 认证JWT │ LangGraph │ │ 搜索过滤 │ 计费/订阅 │ │ 播放进度 │ TTS熔断器 │ ────────┼───────────┼─────────────┤ 低影响 │ P4 │ P3 │ │ BGM列表 │ 视频合成 │ │ 模板库 │ 音频合并 │ └───────────┴─────────────┘ 低复杂度 高复杂度 ``` **P1(第一批,本周)**:计费/订阅 → TTS熔断器 → LangGraph 状态机 **P2(第二批,下周)**:认证JWT → 搜索 → 播放进度 **P3(第三批)**:视频合成 → 音频合并 → 支付 **P4(第四批)**:BGM → 模板 → 分类 ### 3.2 P1 核心模块测试设计 #### 3.2.1 订阅计费模块(subscription) ```typescript // tests/unit/server/subscription/quota-checker.test.ts describe('QuotaChecker', () => { describe('checkAudioQuota', () => { // 正常场景 it('免费用户 10分钟内 应通过', () => {}); it('专业版用户 100分钟内 应通过', () => {}); it('超额后按单价计费', () => {}); // 边界值 it('刚好等于配额上限', () => {}); it('超出配额 1 秒', () => {}); it('配额用尽后再生成', () => {}); // 异常场景 it('用户不存在时抛出 UserNotFoundError', () => {}); it('配额已用尽且不支持超额', () => {}); it('并发扣除配额不超扣', () => {}); // 时间边界 it('跨月重置配额', () => {}); it('会员到期后降级为免费配额', () => {}); }); describe('calculateCost', () => { it('免费套餐不收费', () => {}); it('字数 ÷ 语速 = 分钟数(四舍五入)', () => {}); it('超额部分按单价计算', () => {}); it('计算结果精确到分', () => {}); }); }); ``` #### 3.2.2 TTS 熔断器(circuit-breaker) ```typescript // tests/unit/server/tts/circuit-breaker.test.ts describe('TTS CircuitBreaker', () => { describe('状态转换', () => { it('初始状态为 CLOSED', () => {}); it('连续失败 3 次 → OPEN', () => {}); it('OPEN 状态拒绝所有请求', () => {}); it('冷却 60 秒后 → HALF_OPEN', () => {}); it('HALF_OPEN 成功 → CLOSED', () => {}); it('HALF_OPEN 失败 → 回到 OPEN', () => {}); }); describe('Provider 轮转', () => { it('主 Provider 故障时自动切换', () => {}); it('所有 Provider 都故障时抛出 AllProvidersDownError', () => {}); it('轮转顺序公平', () => {}); }); describe('健康检查', () => { it('定时检查 OPEN 状态 Provider', () => {}); it('健康检查失败重置冷却时间', () => {}); }); }); ``` #### 3.2.3 LangGraph 状态机 ```typescript // tests/unit/server/book-generator/state-machine.test.ts describe('BookGenerator StateGraph', () => { describe('状态流转', () => { it('INIT → DEEP_PLAN → RICH_OUTLINE → CONTENT', () => {}); it('CONTENT → CONTINUITY_EDIT → QUALITY_CHECK', () => {}); it('QUALITY_CHECK 不通过 → REWRITE(最多3次)', () => {}); it('第 4 次 REWRITE 仍失败 → 标记失败', () => {}); }); describe('并行写作节点', () => { it('多个章节并行生成', () => {}); it('并行节点全部完成后才进入下一步', () => {}); it('某章节失败不影响其他章节', () => {}); }); describe('状态持久化', () => { it('中断后可从检查点恢复', () => {}); it('恢复后从正确节点继续', () => {}); }); describe('异常处理', () => { it('LLM API 超时时重试', () => {}); it('生成内容为空时重新生成', () => {}); it('用户取消时优雅终止', () => {}); }); }); ``` ### 3.3 Mock 策略 ```typescript // tests/helpers/mocks/llm-mock.ts /** * LLM Mock 工厂 * 所有单元测试中禁止调用真实 AI API */ export function createLLMMock() { return { invoke: vi.fn().mockResolvedValue({ content: JSON.stringify({ title: '测试书籍标题', outline: [ { chapter: 1, title: '第一章', summary: '章节摘要' } ] }) }), stream: vi.fn().mockImplementation(async function* () { yield { content: '模拟流式输出' }; }) }; } /** * TTS Mock 工厂 */ export function createTTSMock() { return { synthesize: vi.fn().mockResolvedValue({ audioUrl: 'https://mock-cdn.example.com/audio/test.mp3', duration: 120, // 秒 size: 2048000 // 字节 }) }; } /** * 支付 Mock 工厂 */ export function createPaymentMock() { return { createOrder: vi.fn().mockResolvedValue({ orderNo: 'MOCK202605190001', qrCode: 'https://mock-pay.example.com/qr/xxx' }), verifyCallback: vi.fn().mockResolvedValue({ verified: true }) }; } ``` --- ## 四、集成测试设计 ### 4.1 数据库集成测试 ```typescript // tests/integration/db/prisma-crud.test.ts describe('Prisma CRUD 集成测试', () => { let db: PrismaClient; beforeAll(async () => { db = new PrismaClient({ datasources: { db: { url: process.env.TEST_DATABASE_URL } } }); await db.$connect(); }); afterAll(async () => { await db.$disconnect(); }); // 每个测试包裹在事务中,测试完回滚 beforeEach(async () => { await db.$executeRawUnsafe('BEGIN'); }); afterEach(async () => { await db.$executeRawUnsafe('ROLLBACK'); }); describe('User CRUD', () => { it('创建用户并查询', async () => {}); it('更新用户会员等级', async () => {}); it('软删除用户', async () => {}); it('唯一索引冲突 - 重复手机号', async () => {}); }); describe('Book + Chapter 关联查询', () => { it('创建书籍时级联创建章节', async () => {}); it('查询书籍时包含所有章节', async () => {}); it('删除书籍时级联删除章节', async () => {}); }); describe('事务', () => { it('配额扣除+音频创建应在同一事务', async () => {}); it('事务失败时全部回滚', async () => {}); it('并发事务不互相干扰', async () => {}); }); }); ``` ### 4.2 API 流程集成测试 ```typescript // tests/integration/api/book-generator-flow.test.ts describe('书籍生成完整流程集成测试', () => { let app: Koa; let authToken: string; let testUserId: number; beforeAll(async () => { // 启动测试服务器(使用测试数据库) app = await createTestApp(); // 创建测试用户并获取 token const { token, userId } = await createTestUser(app); authToken = token; testUserId = userId; }); describe('完整生成流程', () => { it('Step 1: 预估 - POST /api/book-generator/langgraph/estimate', async () => { const res = await request(app.callback()) .post('/api/book-generator/langgraph/estimate') .set('Authorization', `Bearer ${authToken}`) .send({ topic: '人工智能简史', wordCount: 50000 }); expect(res.status).toBe(200); expect(res.body.data.estimatedCost).toBeGreaterThan(0); expect(res.body.data.estimatedDuration).toBeGreaterThan(0); }); it('Step 2: 检测类型 - POST /api/book-generator/langgraph/detect-book-type', async () => { const res = await request(app.callback()) .post('/api/book-generator/langgraph/detect-book-type') .set('Authorization', `Bearer ${authToken}`) .send({ topic: '人工智能简史' }); expect(res.status).toBe(200); expect(['技术', '科普', '学术']).toContain(res.body.data.bookType); }); it('Step 3: 创建 - POST /api/book-generator/langgraph/create', async () => { const res = await request(app.callback()) .post('/api/book-generator/langgraph/create') .set('Authorization', `Bearer ${authToken}`) .send({ title: '测试书籍', topic: '人工智能简史', wordCount: 50000, strategy: 'deep-plan-parallel' }); expect(res.status).toBe(200); expect(res.body.data.bookId).toBeDefined(); }); it('配额不足时应返回错误', async () => { // 模拟配额耗尽 await exhaustQuota(testUserId); const res = await request(app.callback()) .post('/api/book-generator/langgraph/create') .set('Authorization', `Bearer ${authToken}`) .send({ title: '超出配额的书籍', topic: '测试', wordCount: 100000 }); expect(res.status).toBe(402); expect(res.body.error).toMatch(/配额不足/); }); }); }); ``` ### 4.3 支付流程集成测试 ```typescript // tests/integration/api/payment-flow.test.ts describe('支付流程集成测试', () => { describe('创建订单', () => { it('支付宝扫码支付 - 创建订单成功', async () => {}); it('微信支付 - 创建订单成功', async () => {}); it('无效套餐类型返回400', async () => {}); it('重复下单(幂等性检查)', async () => {}); }); describe('支付回调', () => { it('支付宝回调 - 验签通过 → 更新会员', async () => {}); it('支付宝回调 - 验签失败 → 拒绝', async () => {}); it('微信回调 - 解密成功 → 更新会员', async () => {}); it('重复回调(幂等处理)', async () => {}); }); describe('会员状态', () => { it('付费后 memberLevel 正确更新', async () => {}); it('memberExpireAt 正确计算(月付+30天)', async () => {}); it('到期后自动降级为免费', async () => {}); }); }); ``` --- ## 五、端到端测试设计 ### 5.1 关键用户旅程(5条,必须通过才能发版) #### 旅程 1:注册 → 登录 → 首页浏览 ``` 游客访问首页 → 点击注册 → 输入手机号 → 获取验证码 → 注册成功 → 自动登录 → 跳转首页 → 看到音频列表 → 看到底部TabBar ``` **验证点**: - ✅ 注册表单验证(空手机号、错误格式) - ✅ 验证码发送成功 - ✅ 注册后 JWT Token 正确存储 - ✅ 首页加载公开音频列表 - ✅ TabBar 切换正常 #### 旅程 2:免费 TTS 生成 → 播放 ``` 登录用户 → 点击"创建" → 输入文本 → 选择音色 → 点击"生成语音" → 等待合成完成 → 列表出现新音频 → 点击播放 → 音频播放 → 进度条前进 → 封面旋转 ``` **验证点**: - ✅ 文本输入验证(空文本、超长文本) - ✅ 音色选择正常预览 - ✅ 生成进度显示(WebSocket 推送) - ✅ 生成后自动刷新列表 - ✅ 播放器功能正常(暂停/继续/拖动进度) - ✅ 播放速度切换(0.5x / 1x / 1.5x / 2x) #### 旅程 3:书籍生成(完整流水线) ``` 登录用户 → 进入书籍生成 → 输入书名和主题 → 选择生成策略 → 点击"开始生成" → 看到进度(第1步:规划 → 第2步:大纲 → 第3步:写作 → 第4步:编辑) → 生成完成 → 查看章节列表 → 点击某章节 → 查看内容 → 播放章节音频 ``` **验证点**: - ✅ 预估字数/时长正确显示 - ✅ 书籍类型自动检测 - ✅ LangGraph 各阶段进度实时推送 - ✅ 生成完成状态正确 - ✅ 章节内容和音频正确 - ✅ 大纲 JSON 结构完整 #### 旅程 4:购买会员 → 解锁高级功能 ``` 免费用户 → 尝试超额生成 → 提示配额不足 → 跳转会员页 → 选择专业版套餐 → 确认支付 → 显示支付宝二维码 → 模拟支付成功 → 会员等级更新 → 返回生成 → 配额已提升 ``` **验证点**: - ✅ 配额不足时正确提示 - ✅ 会费价格正确显示 - ✅ 支付订单创建成功 - ✅ 支付回调正确处理 - ✅ 会员等级实时更新 - ✅ 新配额立即生效 #### 旅程 5:发布音频 → 首页可见 ``` 用户 → 我的页面 → 选择一个已生成的音频 → 点击"公开" → 确认公开 → 返回首页 → 刷新 → 该音频出现在列表中 → 其他用户(未登录)访问首页 → 也能看到该音频 ``` **验证点**: - ✅ isPublic 字段正确更新 - ✅ 首页重新加载后包含新公开音频 - ✅ 未登录用户可看到公开音频 - ✅ 自己始终能看到自己的音频(无论公开与否) ### 5.2 E2E 测试编写规范 ```typescript // tests/e2e/ui/critical-journeys/journey-03-book-generate.spec.ts import { test, expect } from '@playwright/test'; import { createTestUser, loginAs } from '../../helpers/auth-helper'; test.describe('Journey 3: 书籍生成完整流程', () => { let user: { phone: string; token: string }; test.beforeAll(async ({ browser }) => { user = await createTestUser(browser); }); test('J03-01: 正常生成流程', async ({ page }) => { // === Arrange === await loginAs(page, user); await page.goto('/pages/book-generator/index'); // === Act: 进入创建页 === await page.click('[data-testid="create-book-btn"]'); await expect(page).toHaveURL(/book-generator\/create/); // === Act: 填写表单 === await page.fill('[data-testid="book-title-input"]', '测试书籍:AI 的过去与未来'); await page.fill('[data-testid="book-topic-input"]', '人工智能发展史'); await page.fill('[data-testid="word-count-input"]', '30000'); await page.selectOption('[data-testid="strategy-select"]', 'deep-plan-parallel'); // === Act: 点击预估 === await page.click('[data-testid="estimate-btn"]'); await expect(page.locator('[data-testid="estimate-result"]')).toBeVisible({ timeout: 10000 }); await expect(page.locator('[data-testid="estimated-cost"]')).toContainText(/分钟/); // === Act: 开始生成 === await page.click('[data-testid="start-generate-btn"]'); // === Assert: 进度展示 === await expect(page.locator('[data-testid="progress-stage"]')).toContainText('深度规划', { timeout: 30000 }); await expect(page.locator('[data-testid="progress-stage"]')).toContainText('生成大纲', { timeout: 60000 }); await expect(page.locator('[data-testid="progress-stage"]')).toContainText('写作中', { timeout: 120000 }); // === Assert: 生成完成 === await expect(page.locator('[data-testid="generation-complete"]')).toBeVisible({ timeout: 180000 }); await expect(page.locator('[data-testid="chapter-count"]')).toContainText(/\d+ 章/); // === Assert: 跳转详情 === await page.click('[data-testid="view-detail-btn"]'); await expect(page).toHaveURL(/book-generator\/detail/); await expect(page.locator('[data-testid="chapter-list"] .chapter-item').first()).toBeVisible(); // === Assert: 章节内容 === await page.locator('[data-testid="chapter-list"] .chapter-item').first().click(); await expect(page.locator('[data-testid="chapter-content"]')).not.toBeEmpty(); await expect(page.locator('[data-testid="chapter-audio-player"]')).toBeVisible(); }); test('J03-02: 配额不足时正确提示', async ({ page }) => { // 预先耗尽配额 await exhaustUserQuota(user.token); await loginAs(page, user); await page.goto('/pages/book-generator/create'); await page.fill('[data-testid="book-title-input"]', '超配额测试'); await page.click('[data-testid="start-generate-btn"]'); // 应提示配额不足 await expect(page.locator('[data-testid="quota-error-dialog"]')).toBeVisible(); await expect(page.locator('[data-testid="quota-error-dialog"]')).toContainText('配额不足'); // 应提供升级入口 await expect(page.locator('[data-testid="upgrade-member-btn"]')).toBeVisible(); }); }); ``` ### 5.3 旧 E2E 测试改造计划 | 旧文件 | 问题 | 改造方案 | |--------|------|---------| | `regression/01-14` | 断言 `expect([0,401,404]).toContain(res.code)` | 拆分场景,明确每个用例的预期状态码 | | `regression/23-frontend-e2e.spec.ts` | 32个用例只测页面存在 | 拆分为各模块组件测试,保留关键旅程 | | 6个 `test.skip()` 用例 | 功能已实现但测试没更新 | 修复或删除 skip | | `smoke/api-readonly.spec.ts` | 30个端点只有GET | 补充 POST/PUT/DELETE | | 缺失 14-16, 18-22 | 功能已实现 | 按优先级补全 | --- ## 六、测试基础设施 ### 6.1 测试数据库 ``` 生产环境:MySQL (localhost:3306) → audio_book 库 测试环境:MySQL (localhost:3307) → audio_book_test 库(独立端口) 策略: 1. 每个测试文件的事务隔离(BEGIN/ROLLBACK) 2. 集成测试使用真实 MySQL,非 SQLite 3. 测试前自动运行 Prisma migrate 4. 测试后自动清理测试数据 ``` ### 6.2 测试配置 ```typescript // tests/vitest.config.ts (新建) import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { // 单元测试 include: ['tests/unit/**/*.test.ts'], globals: true, environment: 'node', coverage: { provider: 'v8', reporter: ['text', 'json', 'html'], include: ['server/src/modules/**/*.ts', 'my-uniapp-vue3/src/**/*.ts'], exclude: ['**/*.d.ts', '**/node_modules/**'], thresholds: { lines: 80, branches: 70, functions: 80, statements: 80, // 按模块设置不同阈值 'server/src/modules/subscription/**': { lines: 95, branches: 90 }, 'server/src/modules/book-generator/**': { lines: 75, branches: 65 }, 'server/src/modules/tts/**': { lines: 90, branches: 85 }, } } } }); ``` ### 6.3 全局测试辅助 ```typescript // tests/helpers/db-helper.ts (新建) import { PrismaClient } from '@prisma/client'; /** * 创建测试专用数据库连接 */ export function createTestDb(): PrismaClient { return new PrismaClient({ datasources: { db: { url: process.env.TEST_DATABASE_URL || 'mysql://root:@localhost:3307/audio_book_test' } } }); } /** * 清理测试数据 */ export async function cleanTestData(db: PrismaClient) { const tablenames = await db.$queryRaw>` SELECT tablename FROM pg_tables WHERE schemaname='public' `; // 按外键依赖顺序删除 const deleteOrder = [ 'AlbumAudio', 'PlayRecord', 'Favorite', 'Comment', 'Notification', 'BookChapter', 'Book', 'Audio', 'Album', 'VideoMaterial', 'VideoProject', 'Order', 'User' ]; for (const table of deleteOrder) { await db.$executeRawUnsafe(`DELETE FROM \`${table}\``); } } /** * 测试数据工厂 */ export class TestDataFactory { static async createUser(db: PrismaClient, overrides = {}) { return db.user.create({ data: { phone: `138${String(Math.random()).slice(2, 10)}`, memberLevel: 0, dailyUsage: 0, ...overrides } }); } static async createBook(db: PrismaClient, userId: number, overrides = {}) { return db.book.create({ data: { userId, title: `测试书籍 ${Date.now()}`, description: '测试描述', totalChapters: 5, estimatedWords: 25000, status: 'completed', ...overrides } }); } static async createAudio(db: PrismaClient, userId: number, overrides = {}) { return db.audio.create({ data: { userId, title: `测试音频 ${Date.now()}`, text: '这是测试文本内容', audioUrl: 'https://mock-cdn.example.com/audio/test.mp3', audioDuration: 120, wordCount: 300, voiceId: 'zh_female_qingxin', status: 'completed', isPublic: false, ...overrides } }); } } ``` ### 6.4 报告体系 ``` 测试报告产出物: 1. 覆盖率报告 (coverage/index.html) - 行/分支/函数/语句覆盖率 - 按模块划分的热力图 2. 测试结果报告 (test-results/report/index.html) - Playwright HTML Report(包含截图/trace/video) - 失败用例自动截图 3. AI Summary 报告 (test-results/ai-summary.md) - 保留现有自定义 reporter - 用于 AI Agent 解析 4. 趋势报告 (test-results/trend.json) - 每次运行的覆盖率变化 - 测试用例数量变化 - 失败率趋势 ``` --- ## 七、CI/CD 流水线 ### 7.1 GitHub Actions 流程 ```yaml # .github/workflows/test.yml (新建) name: Test Pipeline on: push: branches: [master, develop] pull_request: branches: [master] jobs: # Job 1: 快速反馈(< 2 分钟) lint-and-unit: runs-on: ubuntu-latest services: mysql: image: mysql:8.0 env: MYSQL_ROOT_PASSWORD: test MYSQL_DATABASE: audio_book_test ports: ['3306:3306'] options: --health-cmd="mysqladmin ping" --health-interval=10s steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: '20' } - name: Install dependencies run: | cd server && npm ci cd ../my-uniapp-vue3 && npm ci - name: ESLint run: | cd server && npx eslint src/ cd ../my-uniapp-vue3 && npx eslint src/ - name: Unit Tests (L0) run: npx vitest run --config tests/vitest.config.ts --reporter=verbose - name: Component Tests (L1) run: npx vitest run --config tests/vitest.component.config.ts - name: Upload Coverage uses: actions/upload-artifact@v4 with: name: coverage-l0-l1 path: coverage/ # Job 2: 集成测试(< 5 分钟) integration: needs: lint-and-unit runs-on: ubuntu-latest services: mysql: image: mysql:8.0 env: MYSQL_ROOT_PASSWORD: test MYSQL_DATABASE: audio_book_test ports: ['3307:3306'] redis: image: redis:7 ports: ['6379:6379'] steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: '20' } - name: Install & Migrate run: | cd server && npm ci npx prisma migrate deploy npm run build - name: Start Server run: | cd server TEST_DATABASE_URL=mysql://root:test@localhost:3307/audio_book_test \ REDIS_URL=redis://localhost:6379 \ npm run start:test & sleep 10 - name: Integration Tests (L2) run: npx vitest run --config tests/vitest.integration.config.ts - name: API E2E Tests (L3) run: npx playwright test --config tests/playwright.config.ts --project=api-e2e # Job 3: UI E2E 测试(每日或手动触发) ui-e2e: needs: integration runs-on: ubuntu-latest if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' services: mysql: { as above } redis: { as above } steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: '20' } - name: Install & Build run: | cd server && npm ci && npx prisma migrate deploy && npm run build cd ../my-uniapp-vue3 && npm ci && npm run build:h5 - name: Start Full Stack run: | cd server && npm run start:test & sleep 5 - name: UI E2E Tests (L4) run: npx playwright test --config tests/playwright.config.ts --project=ui-e2e - name: Upload Playwright Report if: always() uses: actions/upload-artifact@v4 with: name: playwright-report path: test-results/report/ ``` ### 7.2 本地开发测试命令 ```json // package.json scripts 补充 { "scripts": { "test:unit": "vitest run --config tests/vitest.config.ts", "test:unit:watch": "vitest --config tests/vitest.config.ts", "test:component": "vitest run --config tests/vitest.component.config.ts", "test:integration": "vitest run --config tests/vitest.integration.config.ts", "test:api-e2e": "playwright test --config tests/playwright.config.ts --project=api-e2e", "test:ui-e2e": "playwright test --config tests/playwright.config.ts --project=ui-e2e", "test:critical": "playwright test tests/e2e/ui/critical-journeys/", "test:all": "npm run test:unit && npm run test:integration && npm run test:api-e2e", "test:coverage": "vitest run --config tests/vitest.config.ts --coverage", "test:db:setup": "node tests/scripts/setup-test-db.js", "test:db:clean": "node tests/scripts/clean-test-db.js" } } ``` --- ## 八、质量门禁 ### 8.1 发版门禁(全部必须通过) | 门禁 | 标准 | 检查方式 | 不通过后果 | |------|------|---------|-----------| | **Lint 零错误** | ESLint 0 errors | CI 自动检查 | ❌ 禁止合并 | | **类型检查通过** | tsc --noEmit 通过 | CI 自动检查 | ❌ 禁止合并 | | **单元测试通过** | 全部 PASS,覆盖率达标 | CI 自动检查 | ❌ 禁止合并 | | **集成测试通过** | 全部 PASS | CI 自动检查 | ❌ 禁止合并 | | **5条关键旅程通过** | Critical Journeys 全部 PASS | CI 自动检查 | ❌ 禁止发版 | | **Code Review** | 至少1人 Approved | GitHub PR | ❌ 禁止合并 | | **无高危 Bug** | 无 open 的 P0/P1 bug | 项目管理工具 | ⚠️ 需评审 | ### 8.2 覆盖率红线 ```typescript // 全局最低标准 global: { lines: 80, branches: 70, functions: 80, statements: 80 } // 核心模块特殊标准(不达标 → 禁止合并) 'server/src/modules/subscription/**': { lines: 95, branches: 90 } 'server/src/modules/tts/**': { lines: 90, branches: 85 } 'server/src/modules/payment/**': { lines: 90, branches: 85 } 'server/src/modules/book-generator/**': { lines: 75, branches: 65 } ``` ### 8.3 Bug 定级标准 | 级别 | 定义 | 示例 | 发版影响 | |------|------|------|---------| | **P0 致命** | 核心功能不可用/数据丢失/资金损失 | 支付扣款未到账、配额重复扣除 | ❌ 阻塞发版 | | **P1 严重** | 主要功能异常,有 workaround | 某音色合成失败、书籍生成卡住 | ❌ 阻塞发版 | | **P2 一般** | 次要功能异常 | 播放列表排序错误、搜索历史不显示 | ⚠️ 评估 | | **P3 轻微** | UI 样式问题 | 按钮间距不对、颜色偏差 | ✅ 可发版 | --- ## 九、分阶段实施路线图 ### Phase 1:打基础(第 1-2 周) ``` 目标:建立单元测试基础设施,覆盖 P1 核心模块 Week 1: ├── 安装 Vitest + 配置 ├── 搭建 TestDataFactory + DB Helper ├── Mock 工厂(LLM / TTS / Payment) ├── 完成 subscription 模块单元测试(目标覆盖率 95%) └── 完成 tts/circuit-breaker 单元测试(目标覆盖率 90%) Week 2: ├── 完成 book-generator/state-machine 单元测试 ├── 完成 payment 模块单元测试 ├── 创建 CI workflow 文件 ├── 建立覆盖率达到标检查 └── 团队 Code Review 规范培训 ``` ### Phase 2:补集成(第 3-4 周) ``` 目标:API 集成测试 + 数据库集成测试 Week 3: ├── 数据库集成测试(Prisma CRUD + 事务) ├── auth-flow 集成测试(注册→登录→Token刷新) ├── book-generator-flow 集成测试(完整流水线) └── tts-flow 集成测试(合成→存储→配额扣除) Week 4: ├── payment-flow 集成测试(下单→支付→回调→会员更新) ├── subscription-flow 集成测试(套餐切换→配额计算→超额) ├── publish-flow 集成测试 └── 性能基准测试(关键 API 响应时间基线) ``` ### Phase 3:优 E2E(第 5-6 周) ``` 目标:修复旧 E2E + 新增关键旅程 Week 5: ├── 修复所有 test.skip() 用例 ├── 改造回归测试断言(去除多值断言) ├── 实现 Journey 1-3(注册→TTS→书籍生成) └── 前端页面添加 data-testid 属性 Week 6: ├── 实现 Journey 4-5(购买→发布) ├── 补充缺失的回归测试模块(14-16, 18-22) ├── 集成 CI 自动运行 E2E └── 建立监控告警(失败率 > 5% 告警) ``` ### Phase 4:持续改进(长期) ``` 目标:测试文化建立 + 持续优化 ├── 每 Sprint 新增功能必须有对应测试 ├── 每月审查覆盖率趋势 ├── 每季度执行一次性能测试 ├── 建立测试数据匿名化机制 ├── 引入变异测试(Mutation Testing) └── 探索 AI 辅助测试用例生成 ``` ### 各阶段里程碑 ``` Week 1-2 Week 3-4 Week 5-6 长期 │ │ │ │ 覆盖率: 0% ──▶ 30% ────▶ 55% ────▶ 70% ────▶ 80%+ │ │ │ │ 里程碑: 基础建立 集成就绪 E2E 完整 文化形成 │ │ │ │ 可发版: ❌ ⚠️ ✅ ✅ ``` --- ## 十、附录 ### A. data-testid 命名规范 ``` 格式: {模块}-{组件}-{元素} bookshelf: data-testid="book-create-btn" data-testid="book-title-input" data-testid="book-list-item" player: data-testid="player-play-btn" data-testid="player-progress-bar" data-testid="player-speed-select" member: data-testid="member-plan-card-basic" data-testid="member-upgrade-btn" general: data-testid="toast-success" data-testid="toast-error" data-testid="loading-spinner" data-testid="empty-state" ``` ### B. 测试环境变量 ```bash # .env.test NODE_ENV=test TEST_DATABASE_URL=mysql://root:@localhost:3307/audio_book_test REDIS_URL=redis://localhost:6379 SERVER_PORT=3199 AUTH_ENABLED=false # 测试环境关闭认证 TTS_PROVIDER=mock # 使用 Mock Provider LLM_PROVIDER=mock # 使用 Mock LLM PAYMENT_MODE=sandbox # 沙箱模式 OSS_BUCKET=test-bucket ``` ### C. 常用测试命令速查 ```bash # 开发时使用 npm run test:unit:watch # 监听模式运行单元测试 npm run test:unit -- --reporter=verbose # 详细输出 # 提交前使用 npm run test:all # 运行 L0+L1+L2+L3 npm run test:coverage # 生成覆盖率报告 # 发版前使用 npm run test:critical # 运行 5 条关键旅程 E2E npm run test:all -- --coverage # 全覆盖 + 覆盖率检查 # 故障排查 npx playwright test --debug # Playwright 调试模式 npx playwright show-report # 查看上次测试报告 npx playwright test --trace on # 开启 trace 追踪 ``` ### D. 参考资源 | 资源 | 链接 | |------|------| | Vitest 文档 | https://vitest.dev/ | | Playwright 文档 | https://playwright.dev/ | | Testing Library 最佳实践 | https://testing-library.com/docs/ | | Martin Fowler - 测试金字塔 | https://martinfowler.com/bliki/TestPyramid.html | | Google 测试博客 | https://testing.googleblog.com/ | --- > **文档维护**: 本文档由测试负责人维护,每次测试体系重大变更后更新。 > **下次审查**: 2026-06-19