TESTING_STRATEGY.md 38 KB

Audio CodeBuddy 测试体系设计文档

版本: v1.0
作者: 测试负责人
日期: 2026-05-19
状态: 正式发布


目录

  1. 现状评估
  2. 测试体系架构
  3. 单元测试设计
  4. 集成测试设计
  5. 端到端测试设计
  6. 测试基础设施
  7. CI/CD 流水线
  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)

// 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)

// 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 状态机

// 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 策略

// 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 数据库集成测试

// 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 流程集成测试

// 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 支付流程集成测试

// 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 测试编写规范

// 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 测试配置

// 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 全局测试辅助

// 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<Array<{ tablename: string }>>`
    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 流程

# .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 本地开发测试命令

// 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 覆盖率红线

// 全局最低标准
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. 测试环境变量

# .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. 常用测试命令速查

# 开发时使用
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