# 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<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 流程

```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
