# 测试方法论和最佳实践

基于 HuntAndSeek EndGame 埋点的成功测试经验，总结出以下标准化流程和方法论。

## 核心原则

**全面性 × 深度 × 可操作性 = 高质量埋点审查**

- **全面性**: 每个字段都要验证，不遗漏任何细节
- **深度**: 不仅检查字段存在，更要验证含义和计算逻辑
- **可操作性**: 提供具体的问题定位、修复建议和测试场景

## 测试方法论：三层验证法

### 第一层：字段完整性验证（基础）

**目标**: 确保所有需求字段都已实现，类型正确

**方法**:
1. 创建对比表格，逐一对照需求文档
2. 检查字段类型匹配（STRING→string, NUMBER→int32/int64）
3. 验证 JSON 标签正确（`json:"field_name"`）
4. 识别多余或缺失的字段

**输出示例**:
```markdown
| 需求字段 | 类型 | 实现字段 | 实现类型 | 状态 | 位置 |
|---------|------|----------|----------|------|------|
| game_type | NUMBER | GameType | int32 | ✅ | sensor.go:120 |
| rid | STRING | Rid | int32 | ⚠️ | sensor.go:121 |
```

---

### 第二层：字段含义验证（重要）⭐

**目标**: 确保字段的业务含义与需求定义一致

**方法**:
1. **对照需求定义**，理解字段的真实业务含义
2. **逻辑推导验证**，检查计算公式是否符合业务语义
3. **参数来源追踪**，确认数据来源正确

**案例（HuntAndSeek）**:
```
需求: survive_score = 作为躲藏者获得的总积分
代码: SurviveScore = totalScore - playerStat.GhostScore

逻辑推导:
  - totalScore = 所有回合积分总和 ✅
  - GhostScore = 作为猎人时的积分 ✅
  - 推导: 总分 - 猎人分 = 躲藏者分 ✅
结论: 含义正确 ✅
```

**关键技巧**:
- 对于"总XX"字段，验证是否使用循环累加
- 对于"XX回合数"字段，验证累加时机是否正确
- 对于减法计算，验证推导逻辑是否成立

---

### 第三层：计算逻辑验证（核心）⭐⭐⭐

**目标**: 验证计算公式正确、累加逻辑完整、分支覆盖全面

#### 3.1 累加逻辑验证

**检查要点**:
- [ ] 是否有循环累加（`for` + `+=`）
- [ ] 累加时机是否正确（回合结束/游戏结束）
- [ ] 累加条件是否准确（身份判断/状态判断）

**正确示例**:
```go
// ✅ HuntAndSeek - GhostScore 累加
// game.go:703-704 - 每回合结束时
if p.Role == int32(pbHAS.RoleType_RoleTypeGhost) {
    playerStat.GhostScore += roundInfo.Score  // 猎人回合积分累加
    playerStat.GhostRoundCount++  // 猎人回合数累加
}
```

**错误示例**:
```go
// ❌ 只取当前回合分数
TotalScore: roundInfo.Score

// ✅ 应该累加所有回合
totalScore := int32(0)
for _, roundInfo := range player.RoundInfos {
    totalScore += roundInfo.Score
}
```

---

#### 3.2 枚举字段完整性验证

**检查要点**:
- [ ] 识别所有可能的枚举值
- [ ] 逐值验证实现分支
- [ ] 检查是否有遗漏的枚举值
- [ ] 验证字符串精确匹配

**验证步骤**:

**步骤1: 识别所有枚举值**
```markdown
从需求文档提取:
- stage: 1 | 2 | 3 | 4 | 5  ← 5个值
- identity: "猎人" | "躲藏者"  ← 2个值
- user_state: 1 | 2  ← 2个值
```

**步骤2: 逐值验证**
```go
// ✅ HuntAndSeek - stage 枚举完整实现
switch g.State {
case GameStatePrepareForGame:
    return 1  // ✅ 分配身份
case GameStateHide:
    return 2  // ✅ 躲藏阶段
case GameStateBreakIn:
    return 3  // ✅ 转场阶段
case GameStateGaming:
    if !player.IsAlive() {
        return 5  // ✅ 死亡等待
    }
    return 4  // ✅ 狩猎阶段
default:
    return 0  // ✅ 有 default 分支
}
```

**步骤3: 识别危险信号**
```go
// ⚠️ 危险信号1: 只有 if，没有 else
if condition {
    return "值1"
}
// ❌ 缺少其他枚举值的处理

// ⚠️ 危险信号2: 固定返回值
func getMode() string {
    return "10521"  // ❌ 没有判断逻辑，只返回一个值
}
```

---

#### 3.3 特殊场景逻辑验证

**强退场景**:
```go
// ✅ 正确处理强退场景
if player.ForceQuitTimeMs > 0 {
    param.JoinedRoundNum = playerStat.ForceQuitRound  // 使用强退时的回合
    param.EventDuration = (player.ForceQuitTimeMs - gameInfo.CreateTimestampMs) / 1000  // 使用强退时间
} else {
    param.JoinedRoundNum = gameInfo.CurrentRound  // 使用当前回合
    param.EventDuration = (now - gameInfo.CreateTimestampMs) / 1000  // 使用当前时间
}
```

**时间单位转换**:
```go
// ✅ 正确的单位转换
EventDuration: (now - startTime) / 1000  // 毫秒 → 秒

// ❌ 遗漏单位转换
EventDuration: now - startTime  // 直接使用毫秒，需求要秒
```

---

## 完整调用链追踪方法

**目标**: 理解数据从产生到上报的完整流程

### 追踪模板

```
游戏事件触发
  ↓ [触发时机: 何时触发]
游戏逻辑层计算
  ↓ [计算位置: game.go / fsm.go]
构造埋点参数
  ↓ [参数来源: 哪些字段，如何计算]
调用埋点函数
  ↓ [调用位置: service.go / fsm.go]
埋点定义层转换
  ↓ [转换逻辑: sensor.go]
上报埋点
```

### 追踪示例（HuntAndSeek EndGame）

```
游戏结束 (GameStateGameOver)
  ↓
fsm.go:sendSensorEndGame() (347行)
  ↓ 循环遍历所有玩家
  ↓
计算 totalScore (353-356行)
  - for _, roundInfo := range player.RoundInfos
  - totalScore += roundInfo.Score
  ↓
获取 playerStat (357行)
  - GhostScore: 猎人回合积分累加 (game.go:703)
  - GhostRoundCount: 猎人回合数累加 (game.go:704)
  ↓
构造 EndGameParam (358-382行)
  - SurviveScore = totalScore - GhostScore
  - AttackScore = GhostScore
  - SurviveNum = CurrentRound - GhostRoundCount
  - TotalAttackNum = GhostRoundCount
  - 区分强退/正常场景
  ↓
huntandseek.EndGame(uid, param) (384行)
  ↓
sensor.go:EndGame() (89行)
  - 计算衍生字段 (user_state, identity, title_id)
  - 转换类型 (stage: int32 → string)
  ↓
SensorCustomizedProperties("EndGame", uid, data)
  ↓
埋点上报
```

---

## 报告生成标准

### 必备章节

1. **基本信息**
   - 游戏名、game_type、埋点名称
   - 文件位置（sensor.go 和调用位置）

2. **字段完整性检查表**
   - 所有字段的对比表格
   - 类型匹配状态
   - 问题标识

3. **逐字段详细分析**（核心）
   - 需求定义
   - 代码实现
   - 逻辑推导（对于计算字段）
   - 结论（✅/⚠️/❌）

4. **调用链路图**
   - 完整的数据流图
   - 标注关键计算点

5. **发现的问题**
   - 问题编号、严重程度、位置
   - 问题描述、影响分析
   - 修复建议（带代码示例）

6. **测试建议**
   - 基础场景测试
   - 特殊场景测试（强退、边界）
   - 具体的测试步骤和预期结果

7. **总体评估**
   - 字段完整性百分比
   - 类型正确性百分比
   - 逻辑正确性评估
   - 问题汇总

---

## 关键字段深度分析模板

对于重要的计算字段，使用以下模板：

```markdown
### {字段名}（{字段说明}）

**需求定义**: {从需求文档复制的原文}

**代码实现**:
\`\`\`go
{实际代码片段}
\`\`\`

**逻辑推导**:
- {前提1} ✅/❌
- {前提2} ✅/❌
- 推导: {逻辑推理过程} ✅/❌

**累加逻辑** (如果适用):
\`\`\`go
// 累加代码位置和逻辑
{累加代码片段}
\`\`\`

**结论**: ✅/⚠️/❌ {结论说明}
```

---

## 测试场景设计方法

### 基础场景覆盖

1. **正常结束场景**
   - 验证所有字段都有值
   - 验证正常流程的字段值正确

2. **强退场景**（⭐⭐⭐ 最重要，容易遗漏）
   - 第1回合强退
   - 中间回合强退
   - 最后回合强退
   - 不同身份时强退（躲藏者/猎人）
   - 不同阶段强退（分配身份/躲藏/转场/狩猎/死亡）
   - **关键验证点**: 时间、回合数、身份字段是否使用 ForceQuit 相关值

3. **异常场景**
   - 网络断开
   - 超时判定
   - 被踢出房间

4. **边界场景**
   - 第一回合
   - 最后回合
   - 单回合游戏
   - 最大回合数游戏

### 多回合/身份切换场景（重要）

**示例（HuntAndSeek）**:
```
场景: 3回合游戏，身份切换
- 第1回合: 躲藏者 → 获得 10 分
- 第2回合: 猎人 → 获得 20 分
- 第3回合: 躲藏者 → 获得 15 分

预期结果:
- total_score = 45
- attack_score = 20 (第2回合)
- survive_score = 25 (第1+第3回合)
- total_attack_num = 1
- survival_num = 2
- identity = "躲藏者" (开局身份)
```

### 枚举值完整性测试

对于枚举字段，设计测试用例覆盖所有可能值：

**示例（stage 字段）**:
| 测试场景 | 强退时机 | 期待stage |
|---------|---------|----------|
| 分配身份阶段强退 | GameStatePrepareForGame | "1" |
| 躲藏阶段强退 | GameStateHide | "2" |
| 转场阶段强退 | GameStateBreakIn | "3" |
| 狩猎阶段强退(存活) | GameStateGaming + IsAlive | "4" |
| 狩猎阶段强退(死亡) | GameStateGaming + !IsAlive | "5" |

---

## 常见陷阱和避免方法

### 陷阱1: 只检查字段存在，不验证含义

**错误做法**:
```
✅ survive_score 字段存在
→ 结论: 正确
```

**正确做法**:
```
✅ survive_score 字段存在
✅ 需求: 作为躲藏者获得的总积分
✅ 代码: totalScore - GhostScore
✅ 推导: 总分 - 猎人分 = 躲藏者分
→ 结论: 含义正确，计算逻辑正确
```

---

### 陷阱2: 遗漏枚举字段的某些值

**错误做法**:
```
检查 stage 字段:
- 看代码返回 "1" ✅
- 结论: 正确
```

**正确做法**:
```
检查 stage 字段 (需求: 5个枚举值)
- [ ] 代码能返回 "1" 吗? ✅ (GameStatePrepareForGame)
- [ ] 代码能返回 "2" 吗? ✅ (GameStateHide)
- [ ] 代码能返回 "3" 吗? ✅ (GameStateBreakIn)
- [ ] 代码能返回 "4" 吗? ✅ (GameStateGaming + IsAlive)
- [ ] 代码能返回 "5" 吗? ✅ (GameStateGaming + !IsAlive)
→ 结论: 所有枚举值实现完整 ✅
```

---

### 陷阱3: 忽略累加逻辑的验证

**错误做法**:
```
✅ attack_score = playerStat.GhostScore
→ 结论: 正确
```

**正确做法**:
```
✅ attack_score = playerStat.GhostScore

追踪累加逻辑:
→ game.go:703 - 每回合结束时
→ if p.Role == RoleTypeGhost:
→     playerStat.GhostScore += roundInfo.Score  ✅
→ 累加时机: 回合结束 ✅
→ 累加条件: 身份为猎人 ✅

→ 结论: 累加逻辑正确，时机正确 ✅
```

---

### 陷阱4: 字段名拼写错误未被发现 ⭐⭐⭐

**错误做法**:
```
检查 survival_num 字段:
- 看需求文档: survival_num ✅
- 看代码: survive_num ✅
- 肉眼判断: "差不多" → 标记为正确 ❌
```

**正确做法**:
```
检查 survival_num 字段:
□ 从需求文档复制: survival_num
□ 从代码复制 JSON 标签: survive_num
□ 逐字符精确对比:
  - s-u-r-v-i-v ✅ (前6个字母相同)
  - al vs e ❌ (第7-8个字母不同！)
□ 发现问题: 少了 "al" 两个字母
□ 严重程度: 🔴 Critical (数数平台收不到数据)
□ 修复: json:"survive_num" → json:"survival_num"
```

**为什么是Critical级别**:
- 字段名不匹配 = 数据完全丢失
- 数数平台期望 `survival_num`，服务端上报 `survive_num`
- 无法匹配，所有该字段的数据都丢失
- 影响数据分析和产品决策

**预防措施**:
1. ✅ 禁止手动输入字段名，必须复制粘贴
2. ✅ 使用 diff 工具或脚本进行精确字符串匹配
3. ✅ 在审查报告中逐字段列出需求名 vs 实际名
4. ✅ 特别标注易混淆的字段（survive/survival, attack/atack等）

---

### 陷阱5: 遗漏强退等特殊场景测试 ⭐⭐⭐

**错误做法**:
```
测试 survival_num 字段:
□ 正常结束场景: 3回合游戏 → survival_num = 2 ✅
□ 身份切换场景: 5回合游戏 → survival_num = 3 ✅
□ 结论: 字段计算正确 ✅

❌ 问题: 只测试了正常场景，没测试强退场景！
```

**正确做法**:
```
测试 survival_num 字段:

□ **正常场景测试**
  - [ ] 正常结束: 3回合，身份躲藏者-猎人-躲藏者
  - [ ] 预期: survival_num = 2 ✅

□ **边界场景测试**
  - [ ] 第1回合: survival_num = 0 或 1
  - [ ] 单回合: survival_num = 0 或 1

□ **强退场景测试** ⭐ (之前遗漏！)
  - [ ] 第2回合强退:
    * 游戏总共5回合，玩家第2回合强退
    * 第1回合躲藏者，第2回合猎人
    * 预期: survival_num = 1, round = 2
    * 验证公式: survival_num + total_attack_num = round
  - [ ] 发现问题: ❌
    * 代码使用 CurrentRound (5) 而非 ForceQuitRound (2)
    * 实际: survival_num = 4（错误！）
    * 验证公式失效: 4 + 1 ≠ 2

□ 结论: 正常场景正确，强退场景有 Bug
```

**案例 - survival_num 强退Bug**（HuntAndSeek 项目）:

**问题代码** (fsm.go:372):
```go
// ❌ 错误: 强退时也使用游戏总回合数
SurviveNum: gameInfo.CurrentRound - playerStat.GhostRoundCount

// 场景: 游戏总共5回合，玩家第2回合强退
// CurrentRound = 5 (游戏总回合)
// GhostRoundCount = 1 (第2回合是猎人)
// survival_num = 5 - 1 = 4 ❌ 错误！玩家只参与了1回合躲藏者
```

**正确实现**:
```go
// ✅ 正确: 区分强退和正常场景
SurviveNum: func() int32 {
    if player.ForceQuitTimeMs > 0 {
        // 强退: 使用玩家实际参与的回合数
        return playerStat.ForceQuitRound - playerStat.GhostRoundCount
    }
    // 正常: 使用游戏总回合数
    return gameInfo.CurrentRound - playerStat.GhostRoundCount
}()

// 强退场景:
// ForceQuitRound = 2 (玩家参与了2回合)
// GhostRoundCount = 1 (第2回合是猎人)
// survival_num = 2 - 1 = 1 ✅ 正确！
```

**为什么容易遗漏**:
```
正常思维:
1. 看代码: CurrentRound - GhostRoundCount ✅
2. 测试正常场景: 3回合游戏，结果正确 ✅
3. 结论: 逻辑正确 ✅

遗漏点:
❌ 没有主动设计强退测试场景
❌ 假设 CurrentRound 总是等于玩家参与回合数
❌ 没有验证在强退情况下公式是否仍然成立
```

**强退场景的特殊性**:
- **时间字段**: 使用 `ForceQuitTimeMs` 而非当前时间
- **回合字段**: 使用 `ForceQuitRound` 而非 `CurrentRound`
- **身份字段**: 使用 `ForceQuitRole` 记录强退时身份
- **阶段字段**: 使用 `ForceQuitStage` 记录强退时阶段

**必须测试的特殊场景清单**:
```markdown
□ **强退场景** ⭐⭐⭐
  - [ ] 第1回合强退
  - [ ] 中间回合强退
  - [ ] 不同身份时强退（躲藏者/猎人）
  - [ ] 不同阶段强退（分配身份/躲藏/狩猎/死亡）

□ **异常场景**
  - [ ] 网络断开
  - [ ] 超时判定
  - [ ] 被踢出房间

□ **边界场景**
  - [ ] 第1回合
  - [ ] 最后一回合
  - [ ] 单回合游戏
  - [ ] 最大回合数

□ **多状态切换场景**
  - [ ] 频繁切换身份
  - [ ] 多次死亡复活
```

**验证公式的重要性**:
```
设计验证公式:
survival_num + total_attack_num = round

好处:
✅ 在所有场景下都应成立
✅ 可以快速发现计算错误
✅ 强退场景下如果公式失效，说明有 Bug

实例:
正常场景: 3 + 2 = 5 ✅
强退场景: 4 + 1 = 5 ≠ 2 ❌ 发现问题！
```

**预防措施**:
1. ✅ 设计测试用例时，**必须包含强退场景**
2. ✅ 对于时间/回合数等字段，**主动验证强退分支**
3. ✅ 设计验证公式，在多种场景下验证
4. ✅ 代码审查时，搜索 `ForceQuit` 相关字段和逻辑
5. ✅ 不要假设"正常场景正确=所有场景正确"

**检查方法**:
```bash
# 搜索强退相关字段
grep -n "ForceQuit" sensor.go fsm.go game.go

# 检查是否有条件分支
grep -B5 -A5 "ForceQuitTimeMs\|ForceQuitRound" fsm.go
```

**教训总结**:
> **必须主动设计并测试强退等特殊场景！**
>
> 正常场景测试通过不代表所有场景都正确。
> 强退、异常、边界等特殊场景往往有不同的代码分支，
> 必须逐一设计测试用例验证，不能依赖"应该没问题"的假设。

---

## 效率提升技巧

### 1. 并行验证

同时追踪相关字段，避免重复查找：
```
一次性追踪:
- GhostScore (attack_score)
- GhostRoundCount (total_attack_num)
→ 都在 game.go:703-704 同时累加
```

### 2. 使用 Grep 精准定位

```bash
# 找累加逻辑
Grep: "GhostScore.*\+="

# 找字段定义
Grep: "type.*Stat.*struct"

# 找调用点
Grep: "huntandseek\.EndGame\("
```

### 3. 创建验证清单

在测试前创建清单，逐项勾选：
```
□ 字段完整性检查
  - [ ] 18/18 字段存在
  - [ ] 类型全部匹配
□ 计算字段验证
  - [ ] total_score 累加逻辑
  - [ ] survive_score 含义推导
  - [ ] attack_score 累加时机
□ 枚举字段验证
  - [ ] stage 5个值全部实现
  - [ ] identity 2个值全部实现
□ 特殊场景验证
  - [ ] 强退逻辑完整
  - [ ] 时间单位正确
```

---

## 质量标准

**优秀埋点测试报告应该达到**:

✅ **完整性**: 所有字段都验证，无遗漏
✅ **深度**: 不仅检查字段存在，还验证含义和计算逻辑
✅ **可追溯性**: 提供完整的调用链路图和代码位置
✅ **可操作性**: 问题有具体的修复建议和代码示例
✅ **可测试性**: 提供具体的测试场景和预期结果
✅ **清晰性**: 使用表格、图表、代码片段增强可读性

---

## 快速检查清单（测试完成前）

**报告发出前，确认以下内容**:

- [ ] 字段完整性表格是否包含所有字段
- [ ] **JSON 字段名是否逐个精确对比需求文档**（不能凭肉眼判断）⭐
- [ ] 重要计算字段是否有逻辑推导
- [ ] 累加字段是否验证了累加逻辑
- [ ] 枚举字段是否逐值验证
- [ ] **是否主动设计并测试强退场景**（不能只测正常场景）⭐
- [ ] 是否提供了完整的调用链路图
- [ ] 问题是否有具体的修复建议
- [ ] 是否提供了测试场景和预期结果（包含强退、边界、异常）
- [ ] 评估部分是否有百分比统计

---

**记住**：目标是确保埋点准确性，以便分析数据可以被信赖用于产品决策。要全面、具体，并始终提供可操作的建议。
