20 游戏测试与质量保障
本章目标:把「测试」从找人肉点游戏的手艺,讲成一条能上门禁的生产线。重点是四件事:质量口径(游戏里什么 bug 最贵)、测试分层(从逻辑单测到机器人对局,每层管什么)、为可测性设计(确定性、时间可控、依赖可替换——服务端的测试成本是设计出来的,不是测出来的)、性能验收(第 19 讲的预算表怎么变成合入门禁)。QA 流程的协作框架参考知识库主线与通用工程实践,本章聚焦游戏特有的部分。
参考书籍:测试分层与可测性设计的方法论参考《Software Engineering at Google》测试章节与《单元测试的艺术》;游戏特有的质量口径与机器人压测实践综合自《百万在线》及线上运维实践。
1. 质量口径:游戏里什么 bug 最贵
通用互联网服务的严重级排序(资损 > 数据损坏 > 可用性)搬到游戏要加两条游戏特有的:
- 崩溃与卡死永远最高级——玩家可以接受玩法平庸,很难原谅闪退,且小游戏平台的崩溃率直接影响渠道评级(第 03 讲的平台约束)。
- 资损类:复制物品、负数金额、多发货——经济系统漏洞的修复成本随扩散时间指数上涨(第 02 讲经济系统、第 18 讲风控都从不同侧讲过)。
- 公平性类:作弊、外挂、判定不一致——竞技玩法的公平事故等同于资损,因为它消耗的是对游戏规则本身的信任。
- 存档与进度丢失:玩家感知等同"我充的钱没了",客服成本与舆情风险双高。
判断:游戏 bug 的修复窗口比普通服务更苛刻——版本节奏是有营销档期的,上线日、活动日、版本直播日的质量门槛事实上更高,这些日期要提前写进测试计划,而不是撞上了再补测。
边界也要说清:QA 不是质量的生产者,是质量的度量者。质量是整条管线(代码规范、评审、自动化、灰度、监控)的产出;把"上线后出 bug"归咎于 QA 测得不够,和把"服务被打挂"归咎于监控不够灵敏,是同一种归因错误。
2. 流程与责任边界:提测、验收、回归、出包
游戏研发的四个质量关卡,各自的准入准出要写成显式标准:
| 关卡 | 责任人 | 准入标准 | 准出标准 |
|---|---|---|---|
| 提测 | 开发→QA | 自测通过、冒烟清单跑绿、测试环境就绪 | QA 确认可测,打回有据 |
| 验收 | 策划/产品 | 功能实现完成 | 按需求单逐条通过,数值曲线符合设计 |
| 回归 | QA+自动化 | 缺陷修复合入 | 修复验证 + 关联用例通过,无新增失败 |
| 出包 | QA+发布 | 版本冻结、遗留缺陷评审通过 | 冒烟通过、包体与权限审查通过(第 17 讲版本可判定性) |
最容易腐化的是提测标准:开发带着"大概能跑"的版本进提测,QA 沦为免费测试工。工程解法是把提测冒烟清单自动化(第 3.4 节),"冒烟不过自动打回"让标准不再依赖人际博弈。
游戏特有的两个衔接点:数值验收要策划与程序共同对账(配置表导出后概率、产出曲线与设计值比对,第 02 讲的概率校验管线在这里成为验收工具);合规验收(实名、适龄、隐私文案)在每次出包前过一遍清单——这是渠道审核被拒的高频原因(第 18 讲合规清单)。
3. 自动化测试分层:金字塔在游戏后端的形态
3.1 逻辑单测:测规则,不测表现
结算公式、掉落概率、状态机迁移、背包边界——这些纯逻辑是单测的主场。游戏后端的特殊纪律是随机与时间不许散落(第 5 节展开),否则逻辑不可单测。数值系统的单测要带统计断言:抽 100 万次验证掉率偏差在容忍带内,单次结果断言不了概率(第 02 讲概率校验脚本就是统计断言接 CI 的先例)。
3.2 协议级回放测试:游戏后端性价比最高的一层
把客户端与服务端的通信录制下来(脱敏后存档),测试时重放请求、比对响应。它一次性解决三个问题:
- 回归:重构后重放旧流量,响应漂移就是行为变更的证据。
- 协议兼容性:新旧版本协议改了字段,重放旧录制能直接判定老客户端还能不能用(第 04 讲协议演进、第 17 讲多端共存判定的自动化抓手)。
- 复现:客诉给来的操作序列变成可重放的录制,"复现不了"的借口少一大半。
边界:回放测试依赖录制时的状态,玩家数据、时间、随机都会让重放漂移——所以回放框架要配套状态注入(录制时存快照与种子,重放时还原),这正是第 5 节可测性设计的用武之地。
3.3 机器人对局:一鱼两吃,但要知道它测不出什么
用机器人玩家打完整对局/流程(登录→匹配→对局→结算),同时服务两个目的:
- 玩法回归:全链路能走通,结算正确,物品入账——比接口单测更接近真实。
- 压测负载:放大机器人规模就是压测(第 13 讲的压测模型),行为拟真度决定压测结论的可信度。
误区是高估 bot 的覆盖面:bot 测得出"能不能玩",测不出"好不好玩"。手感、难度曲线、节奏感这些体验质量,机器人无能为力,这部分永远要留给真人测试与试玩会。另一个坑是 bot 行为过于理想化——真实玩家会断线、挂机、点错,bot 全都不做,压测结论会系统性偏乐观。给 bot 加故障行为(随机断线、超时、乱序)是拟真度的下限要求。
3.4 冒烟测试与发布门禁
冒烟是每个构建的最小通过集:登录、进场景、打一场、买东西、下线,5-10 分钟自动跑完。它的定位是门禁不是保障——冒烟绿了不代表质量好,只代表"值得往下测"。挂接点有两处:CI 构建后(挡住坏构建进提测)、出包前(第 17 讲发布管线的最后一道闸)。冒烟用例要极克制,往里塞用例的冲动会让它从 5 分钟涨到 1 小时,然后所有人都开始绕过它。
4. 兼容性测试:机型矩阵与平台差异
4.1 机型矩阵:按用户分布倒推,不按厂商送测机
矩阵的制定口径:线上用户分布的 Top 机型 + 端型覆盖(旗舰/中端/低端)+ 系统/容器版本覆盖。常见错误是拿办公室里的旗舰机当验收基准——低端机上的崩溃、超时、OOM 在旗舰机复现不出来。第 19 讲首包监控按机型分位数的口径,反过头来就是矩阵更新的依据:P90 之后机型占比涨过阈值就进矩阵。
4.2 平台差异:三个容器三种脾气
第 03 讲铺过的平台约束在测试侧一一对应:iOS 的后台挂起与审核规则、Android 的碎片化(厂商 ROM 对后台与权限的自定义)、小游戏容器的包体限制与 API 缺失。兼容性 bug 的归属判定是排障的第一步:
- 引擎层:特定 GPU/驱动的渲染异常——解法是引擎版本或规避写法,验证靠矩阵最小复现集。
- 资源层:纹理格式、音频编码在特定硬件不支持——资源管线出包时按档产出(第 19 讲 LOD 与贴图分档)。
- 代码层:真机才暴露的时序问题(后台切回、来电打断、低内存警告)——自动化做不了,进真机测试清单。
5. 为可测性设计:确定性、时间、依赖、回放
服务端代码可测不可测,是设计出来的。四条设计纪律:
- 确定性随机:战斗内随机走独立随机源、种子进战斗日志(第 02 讲 3.3 节的完整纪律)。可复现是回放测试、裁决、bug 复现的共同前提——"这个 bug 我复现不了"的最好解药是重放同一颗种子。
- 时间可控:时间是个依赖,不是环境。业务代码里散落
time.Now()的每一处,都是"只能等到第二天才能测"的定时炸弹(活动开始瞬间、跨天结算、赛季切换的 bug 全在这里)。注入时钟接口,测试里拨表。 - 依赖可替换:支付、平台登录、推送这些外部依赖,代码里走接口,测试与预发环境接沙箱。渠道沙箱的行为与生产有差异(尤其支付回调),沙箱通过不等于生产通过——生产环境再用小额真付做一次抽查。
- 对局可回放:种子(随机)+ 输入序列(操作)+ 状态快照(开局)= 完整复现三件套。这也是第 04 讲帧同步回放与第 03 讲快照兜底在测试侧的复用:同一套机制,平时是玩法功能,出事时是取证工具。
判断:这四条的共同点是把不确定性从代码里挤到边界上。随机、时间、外部服务都是天然不确定的,业务逻辑不该继承这些不确定性,否则每写一个测试就要先驯服一次环境。
6. 性能验收:预算表怎么变成门槛
第 19 讲把性能拆成了四本账,本节把它们变成验收门槛:
| 预算 | 验收门槛 | 何时跑 |
|---|---|---|
| 首包时间 | 目标机型矩阵 P50/P90 分位数达标 | 版本出包前,机器人自动跑 |
| 包体大小 | 主包/整包红线(平台规则为准) | CI 每次构建 |
| 内存水位 | 高压场景 N 小时实测无持续增长 | 版本出包前,低端机优先 |
| 服务端容量 | 压测通过线(如单机 3000 人、P95<100ms,第 13 讲口径) | 大版本前 + 架构变更后 |
两条纪律:门槛要写进 CI 与出包检查单,不是写进 wiki——写进 wiki 的门槛会被赶工期悄悄绕过;性能回归要有基线,每次版本与上一版比对分位数,漂移超阈值先定位再合入(和服务端接口的压测基线同口径,附录 A4)。
7. 代码示例
以下代码块是本章设计决策的最小演示实现(非摘自真实项目):一个可注入的时钟、一个确定性随机源、一个协议回放比对器。小节内代码块合并后可通过 go vet 与 go build 编译,并受全库代码块回归测试约束。
可注入的时钟——把"现在几点"变成依赖(第 5 节纪律 2):
// Clock 抽象时间源:生产用系统时钟,测试用假时钟拨表
type Clock interface {
Now() time.Time
}
type systemClock struct{}
func (systemClock) Now() time.Time { return time.Now() }
// fakeClock 只在测试代码里用:手动推进,跨天结算/活动切换的时序 bug 全靠它提前暴露
type fakeClock struct {
current time.Time
}
func (c *fakeClock) Now() time.Time { return c.current }
func (c *fakeClock) Advance(d time.Duration) { c.current = c.current.Add(d) }
// 会话超时判定:不直接调 time.Now,才能被单测覆盖
func sessionExpired(c Clock, lastSeen time.Time, ttl time.Duration) bool {
return c.Now().Sub(lastSeen) > ttl
}确定性随机源——同第 02 讲 3.3 节的纪律:种子决定序列,序列可重放(演示用最简单的可跨语言实现的混合算法):
// DeterministicRand 确定性随机源:相同种子产生相同序列(回放/裁决的前提)
type DeterministicRand struct {
state uint64
}
func NewDeterministicRand(seed uint64) *DeterministicRand {
return &DeterministicRand{state: seed}
}
// next 用 splitmix64:无锁、无依赖、跨语言可复现(客户端用同一算法可校验服务端序列)
func (r *DeterministicRand) next() uint64 {
r.state += 0x9e3779b97f4a7c15
z := r.state
z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9
z = (z ^ (z >> 27)) * 0x94d049bb133111eb
return z ^ (z >> 31)
}
// Roll 先决 d20:返回 [0, n) 的确定性结果
func (r *DeterministicRand) Roll(n int) int {
return int(r.next() % uint64(n))
}协议回放比对器——第 3.2 节回放测试的最小判定骨架(录制解析省略,只看比对逻辑):
// Record 一条脱敏后的录制:请求与当时的服务端响应
type Record struct {
Seq int64
Req string
Want string
}
// Replay 重放录制并比对响应,返回不一致的序号列表
// mismatch 非空 = 行为漂移(回归)或老协议失配(兼容性判定),哪个取决于回放的基线版本
func Replay(recs []Record, handle func(req string) string) []int64 {
var mismatch []int64
for _, rec := range recs {
if got := handle(rec.Req); got != rec.Want {
mismatch = append(mismatch, rec.Seq)
}
}
return mismatch
}8. 实战建议
按角色给行动清单:
- 服务端开发:新模块评审时过一遍第 5 节四纪律(随机/时间/依赖/回放)——可测性在评审时便宜,在联调时昂贵。写结算类逻辑时顺手补统计断言。
- QA:把提测冒烟自动化做成第一个目标;机型矩阵每季度按线上分布校准一次;性能门槛写进出包检查单而不是 wiki。
- 后端负责人:协议每次变更后跑一轮旧版本录制回放(第 3.2 节),兼容性判定自动化,别靠人肉对字段表(第 17 讲版本可判定性)。
- 与客户端协作:客户端崩溃率、首包分位数(第 19 讲)与服务端压测报告放进同一份版本质量报告——质量是一张两端合账(第 19 讲 6 节同款结论,在测试侧依然成立)。
9. 常见陷阱总结
| 陷阱 | 说明 | 缓解 |
|---|---|---|
| 提测标准靠人情 | "大概能跑"就提测,QA 沦为测试工 | 冒烟自动化,不过自动打回 |
| 随机/时间散落业务代码 | 时序与概率 bug 只能"等它发生" | 第 5 节四纪律,评审时把关 |
| 只测旗舰机 | 低端机崩溃与超时全漏 | 机型矩阵按线上分布倒推 |
| 高估 bot 覆盖面 | 手感与节奏问题 bot 永远测不出 | 体验质量留给真人试玩 |
| bot 行为理想化 | 压测结论系统性偏乐观 | bot 加断线/超时/乱序故障注入 |
| 回放不配状态注入 | 重放漂移,回放测试名存实亡 | 录制时存快照与种子 |
| 性能门槛写进 wiki | 赶工期悄悄绕过 | 门槛进 CI 与出包检查单 |
| 沙箱通过即认为生产通过 | 支付回调等沙箱与生产行为有差异 | 生产小额真付抽查 |
10. 小结
本章把测试拆成四层生产线:单测管规则、协议回放管行为漂移与协议兼容、机器人对局管全链路与负载、冒烟管门禁;再把可测性本身变成四条设计纪律——确定性随机、时间可控、依赖可替换、对局可回放。贯穿的主线与第 19 讲一脉相承:质量不是测出来的,是设计与管线的产出;测试的深度,取决于代码对不确定性处理得有多干净。
下一章进入第 21 讲 音频与美术管线协作:资源怎么从美术工具走到引擎,服务端在这条管线里管什么、不管什么。
11. 实战案例:一次「只能等它发生」的时序 bug 逃逸排查
案例为教学示例(非摘自真实项目),数字经过取整简化。它把第 2 节的四关卡、第 3 节的金字塔四层、第 5 节的可测性四纪律与第 9 节陷阱表用到深水区:逐层回答「四层全绿,时序 bug 为什么还是第 4 次上线」——这是测试体系的时间维度排查,也是「随机/时间散落业务代码」那条陷阱的完整展开。
背景与现象:近半年时序类 bug 共 4 起——赛季切换 2 起、限时活动开启 1 起、每日商店刷新 1 起——全部带病出厂,逃逸率 100%,且全部在跨天或活动边界的瞬间被玩家撞见。更疼的是修复验证:每次都要等真实日历跨天才能确认,每起平均 3 轮 × 24h = 72h,4 起累计验证 288 小时。而 QA 每一次回归报告都是全绿:提测通过、验收通过、回归通过、冒烟通过——四关卡一路绿灯,事故照发。
第一步:现象与口径——两个 100% 同时为真,因为分母不同
三个口径摆在一起:
- QA 回归报告:回归通过率 100%——分母是「已写好的用例集」
- 线上事故口径:时序类逃逸 4 起、逃逸率 100%——分母是「时间轴上的全部路径」
- 分母口径差就是 31 条:代码扫描出的跨天判定路径共 31 条,从未有任何一条进过用例集
通过率 100% 与逃逸率 100% 不矛盾:一把没有时间刻度的尺子,量什么都是满的——第 1 节「QA 是度量者,不是生产者」的度量尺本身缺了一维。
第二步:分层归因——金字塔四层逐层过,每层都有时间盲区
代码侧先扫数:time.Now() 直调 147 处,其中跨天判定路径 31 处(21%)。金字塔 4 层逐层对账:
单测层 31 条跨天路径全部依赖真实时钟,测试无法快进——覆盖 0 条
回放层 录制快照是「当时时刻」的即时态,没有「快进到 00:00」的注入点——覆盖 0 条
机器人层 bot 会话生命周期 15 分钟,从不经历跨天——覆盖 0 条
冒烟层 冒烟 5 分钟跑完,天然不覆盖零点边界——覆盖 0 条
合计 31 条跨天路径,四层覆盖率 0 / 31账本收敛:四层对跨天路径的覆盖是 0 / 31;冒烟只跑 5 分钟(第 3.4 节的 5-10 分钟窗口内),bot 会话 15 分钟——都到不了零点。这不是用例写少了,是四层都没有「时间快进」这个注入点。
第三步:根因——缺的不是用例,是时间这个依赖的注入点
- 直接原因:31 条跨天判定路径全部直读系统时钟,没有一处可被测试拨动——「活动开始瞬间、跨天结算、赛季切换」的判定只能等日历走到那里
- 机制原因:第 5 节的 4 条设计纪律里,「时间可控」排第 2 条,却停在评审建议——没有门禁拦直调;第 9 节陷阱表 8 条里的第 2 条早写明「时序与概率 bug 只能等它发生」,写进了表格没写进流程
- 深层原因:第 2 节的 4 个关卡里,回归准出标准是「关联用例通过」——关联用例集里没有跨天用例,绿得理直气壮;第 1 节的版本节奏带着营销档期,档期越近越没人敢把「等到明天」写进计划——口径缺一维,赶工期就把它踩穿
第四步:处置与回填
处置按「收益 / 成本」排序:
- 时钟注入改造:
time.Now()直调 147 → 0(lint 门禁挡直调,业务代码只许依赖 Clock 接口——第 7 节代码示例的形态) - fakeClock 拨表补跨天单测 31 条,31 条路径一一对应
- 时序回归进 CI:每构建拨表跑一轮,验证周期 72h → 10 分钟(432 倍)
- 质量报告加「时间维度覆盖率」口径,与客户端性能报告两端合账(第 8 节同款结论)
复核:随后两个版本时序类逃逸 0 起(此前 4 起);跨天用例集进回归关准出,单轮验证稳定在 10 分钟。
回填清单(呼应第 14 讲复盘回填纪律):
| 回填项 | 归属 | 完成标准 |
|---|---|---|
time.Now() 直调进 lint 门禁,业务代码只许走 Clock 接口(呼应第 5 节纪律 2、第 7 节代码示例) | 服务端 | CI 拦截生效,直调归零 |
| 可测性四纪律进模块评审 checklist(呼应第 8 节服务端开发条) | 服务端/评审 | 新模块评审有记录 |
| 跨天/活动边界用例进回归关准出(呼应第 2 节回归关卡准入准出) | QA | 关联用例集含时间轴维度 |
| 时间维度口径进版本质量报告(呼应第 19 讲两端合账) | QA/数据 | 报告模板改版 |
案例的三个教训
- 时间不是环境,是依赖。第 5 节纪律 2 的原话——
time.Now()散落的每一处,测试都只能以日历为夹具;与第 07 讲案例「时间不是度量,是输入」是同一枚硬币的测试面与生产面 - 金字塔的每一层都可能共享同一个结构性盲区。单测、回放、机器人、冒烟全绿仍然 0/31——层与层独立变绿,不会互相补齐没注入的时间维度;加用例补不了缺注入点的层
- 通过率 100% 与逃逸率 100% 可以同时为真。第 1 节说 QA 是度量者:分母不进尺子,度量越精确越误导——第 13 讲「监控全绿」口径课的测试版