上篇写了《全栈测试》的读书笔记——那是普通软件的测试方法论。这篇开始读一个专门讲游戏测试的教程,22 章,从人工测试讲到强化学习代理。第一章是基础理论。

读之前我有个直觉上的问题:**”游戏测试和普通测试,不就差一个’测的东西是游戏’吗?”**

读完第一章发现不是。差的东西很具体,而且每一条都直指游戏代码和普通应用代码的底层差异。以下是我理解后的梳理。


一、交互复杂性:不是输入 → 输出,是持续碰撞

普通软件的测试模型是清晰的:输入 A → 处理 → 输出 B。测试用例就是”给定 A,验证结果是 B”。

游戏不是这样。游戏的每一帧都在发生多系统的交叉作用。以我写的那个 Go 射击游戏为例,一帧里同时跑的东西:

1
2
3
帧 N: 玩家按空格 → 射击冷却检查 → 生成子弹 → 注册碰撞事件
帧 N+3: 子弹撞到陨石 → 伤害计算 → 分数累加 → Combo 检查 → 陨石分裂
帧 N+5: 小陨石生成 → 方向随机 → 检测出生位置是否与玩家重叠

这不是一条线性链路,是网状交互。教程里给了个 MMO 战斗的复杂度公式:

$$C_{combat} = N_{skills} \times N_{buffs} \times N_{targets} \times N_{positions} \times N_{timing}$$

每一项都是一个维度,乘起来就是状态空间。第一章还算了:一个简化 RPG 角色的理论状态空间大约是 10^60。这个数字大到穷举完全没有可能。

这跟普通软件的区别在哪?一个登录页面,你测”输入正确的用户名密码 → 登录成功”、再测”输入错误 → 提示错误”,两个用例覆盖了核心路径。一个游戏,两万个测试用例可能还没覆盖 1% 的状态空间。


二、实时性:对不是错,晚也是错

普通软件测的是”对不对”。游戏测的是”对不对,以及够不够快“。

60 FPS 意味着每帧不能超过 16.67ms。这 16.67ms 要完成输入处理 + 逻辑更新 + 物理计算 + 渲染 + 同步——任何一个环节拖后腿,帧就掉了。教程给了不同帧目标的细分:

游戏类型 目标帧时间 FPS
竞技游戏 8.33ms 120
动作游戏 16.67ms 60
休闲游戏 33.33ms 30

但关键不在这张表,在于教程说的另一句话:帧率不稳定比低帧率更影响体验。从 60FPS 掉到 30FPS(50% 变化)比从 30FPS 降到 20FPS(33% 变化)更难受——人眼对帧率变化敏感,符合 Weber-Fechner 定律。

这让我想起 Go 游戏里 Tick 计时器那段的取舍——选了帧数当单位、放弃了真实时间,当时说的是”卡顿不会吃掉冷却”。放在这个上下文里看,Tick 方案其实就是在回避实时性的测试难题:如果用真实时间,你测冷却逻辑时还得同时测帧率波动下的表现,测试矩阵直接翻倍

实时性另一个坑是竞态条件:逻辑线程和渲染线程分离,如果同步不当,视觉上会出现”角色瞬移”——不是逻辑算错了,是两个线程读到不同时刻的数据。这种 bug 很难复现——它依赖两个线程的精确执行时序,而时序每帧都略有不同。测 100 次可能只触发 3 次。

教程的常见陷阱列表里单独列了”异步系统的竞态测试”,并用一句话概括了核心困境:在开发环境里永远无法重现的生产环境 bug


三、随机性:你不能”再跑一遍看看”

普通软件大部分场景是确定性的。登录失败,再试一次还是失败——确定性让 bug 可以复现。

游戏里充满随机:暴击、掉落、程序化地图、匹配对手。教程把游戏随机性分了四层:

1
2
3
4
L1: 简单随机(掉落、暴击)     → 二项检验
L2: 参数化生成(属性、数值) → 卡方检验
L3: 结构化生成(地图、关卡) → 连通性验证
L4: 智能生成(任务、剧情) → 规则约束

每一层需要不同的验证手段。L1 的暴击率,你不能”打 10 刀看看出了几次暴击”——教程给了置信区间公式:

$$\text{置信区间} = \hat{p} \pm z_{\alpha/2} \sqrt{\frac{\hat{p}(1-\hat{p})}{n}}$$

验证 10% 的暴击率,至少需要 10000 次攻击才能让置信区间落到 [9.4%, 10.6%]。100 次不够,1000 次勉强——统计验证需要大样本,而大样本意味着测试不能是手动的

教程介绍了三种统计检验的用途分工:

  • 二项检验:测一个概率对不对(暴击率 25% 对不对)
  • 卡方检验:测多个类别分布对不对(掉落表 50/30/15/5 对不对)
  • K-S 检验:测连续数值分布对不对(伤害浮动是不是真的均匀分布)

这三个检验我在读之前完全没概念。普通软件测试不会用到它们,因为普通软件很少有”25% 概率触发某行为”这种设计。


四、边界条件组合爆炸

这是第一章我印象最深的部分,因为它直接解释了为什么有些 bug 线上炸了,测试环境从来没出现过

单个边界条件不难测。HP=0 和 HP=MAX,两个用例。

但边界条件会叠加。教程举的例子:

  • 等级 1 + 最强装备(数值溢出)
  • HP=1 + 吸血效果(除零风险)
  • 坐标 Integer.MAX_VALUE + 移动(坐标回绕)

这些组合在正常测试流程里几乎不可能自然出现——玩家不会 1 级就拿到最强装备。但测试要做的恰恰就是找到这种现实中不太可能、但你必须保证别崩的极端叠加。

让我拿自己的 Go 游戏举例。这三个平时分散的参数:

参数 正常范围 危险的边界
score 0 ~ 几千 接近 Int32 上限(21 亿)
combo_multiplier 1×~16× 10 连以上,512×
meteor_size 1 / 3 / 5 大型陨石 = 5 分

打分公式很简单:

1
score += meteor_size × combo_multiplier

单独测,每个都安全

  • Combo=1,打一个大陨石:score += 5
  • Combo=10,打一个小陨石:score += 512,从几千分开始加,距离溢出还远 ✅
  • 没 combo 但分数很高:每次加 1~5 分,龟速增长,溢不出 ✅

三个边界叠在一起

score = 2,147,483,640(离 Int32 上限差 7 分)
combo = 10 连(512×)
下一个陨石是大号(5 分)

1
2
3
score += 5 × 512
score = 2,147,483,640 + 2,560
= 2,147,486,200 → 超过 Int32.MaxValue

如果代码里没有溢出检查,二进制绕回,分数变成负二十亿。玩家从”马上破纪录”变成”分数崩了”。

为什么正常测试抓不到:手动把 score 调到溢出边缘不会自然地出现在测试流程里。Combo 叠到 10 连你在单独测 combo 机制时跑过,分数涨到几千你在正常游戏测试里自然涨到了——但这两个边界永远不会在同一场测试里同时到达。N 个边界条件各有 M 个取值,理论上要测 M^N 个组合才能覆盖。教程给的解法是正交数组和风险优先级——这部分后面章节会展开。


五、一个开发者读完的感受

第一章不是在讲”怎么测游戏”,而是在讲”游戏为什么难测”。四个特点——交互复杂性、实时性、随机性、状态空间爆炸——每一个都在说同一件事:

普通软件的测试工作量是线性的,游戏测试的工作量是指数级的。

但这话反过来也成立:如果你能理解这四种难度的本质,你写的代码会天然更好测。

举个例子。写完 Go 游戏后我才意识到:Tick 计时器的选择、极坐标生成陨石、Combo 倍率的上限检查——这些设计决策,每一个都在无意中缩小了测试空间。当时我是因为”这样写更简单”做的选择,但换个角度看,我其实是在降低组合复杂度和实时依赖。

这是我读完第一章最大的收获:测试思维和开发思维在底层是相通的。 你不需要成为一个测试工程师才能受益于测试方法论——理解”什么东西难测”,本身就会改变你写代码的方式。


下一章:人工测试的艺术与科学——探索性测试策略、边界测试、Bug 复现技巧。