「HeirCut 开发手记」第九篇。游戏简介见第一篇文末。
做《宫廷理发师》的这一年里,最让我头疼的问题,往往不是哪个检查失败了——失败至少会指出哪里不对。最麻烦的是检查通过了,但它验的根本不是我以为的那样东西。
具体是哪一次最头疼,我已经记不清了。但印象里不止一次是这样:排查时以为问题出在一个方向,查到最后才发现在另一个方向。代码里也因此留下了不少调试日志。
回头翻记录,这样的坑我们踩了十几个。回头看,它们可以归成几种形状。
同一样东西比了两遍
为了给打包瘦身,我们想给一批贴图开压缩,写了个工具:分别导出压缩前、压缩后的图,逐像素比较。结果是每个像素都相同,最大差值为 0。看起来像是「压缩几乎无损,可以放心全量开」。
实际上压缩根本没生效。GPU 的块压缩格式要求图片的宽和高都是 4 的倍数,这批图经过最大尺寸缩放之后,落在了 2048 × 1153 这种尺寸上——差一个像素就压不了,Unity 也不报错,静默退回未压缩的格式。工具比的,是同一张图的两份拷贝。
唯一的破绽是工具两次都打印了「格式 = RGBA32」。之后的判据是:比较压缩效果之前,先断言格式确实变了,变了才比像素。
验的是旧版本
改了代码默认值,运行时还是老数。 给一个已有组件新加一个可序列化字段时,Unity 会在第一次编译时,把当时的代码默认值烘进 prefab 的导入产物里。之后再改代码里的默认值,产物不会重新生成。我们为一个手感参数调了四轮,每一轮都在核对源码,可运行时读的一直是第一版的值——新加的日志都打出来了,唯独这个字段是旧的。之后调参期间一律用常量,定下来再暴露成字段。
测试跑的是上一轮编译的程序集。 命令行模式下改完测试直接跑,Unity 有时会先用磁盘上旧的程序集跑完,退出时才重新编译。症状是失败信息里的期望值在源码里根本搜不到。判据也就是这个:拿失败信息里的字面量去搜源码,搜不到就是旧程序集,重跑。
离线检查读的是源头,游戏读的是成品。 剧情内容要「重建」成数据库,游戏才读得到。有一次内容格式升级了,离线校验直接读源文件,全绿;可仓库里那份重建好的数据库还是旧格式,几十个依赖它的测试一启动就报错。离线通过,不等于成品已经更新。
根本没跑
新测试从来没被执行过。 有一阵我们的回归脚本是按测试类的名字过滤、分块跑的。新加的一个测试类,名字不在任何一块的过滤条件里,于是整轮回归一次都没跑到它——而总数看起来毫无异常。直到有一次整程序集跑,才发现总数是 169,不是 166。判据变成:加了测试,总数就该涨;没涨,就是没跑到。
过滤条件写成了字符串 “null”。 测试工具要求某个过滤参数必须出现,传了字符串 "null",它就去找一个叫 null 的测试,结果是「成功,共 0 项」。看起来全绿,其实一项没跑。之后每次都核对总数。
监视脚本只认成功信号。 等回归跑完的监视脚本,只看「这一块跑完了」的标记有没有出现。有一次编辑器没关,八块测试全部因为「已有一个 Unity 实例在运行」而崩溃退出,监视脚本却把八次崩溃报成了八次跑完。在这之前,同一个脚本还死等过一个已经被删掉的文件,空转了将近四个小时。两次的形状一样:只写了「成功长什么样」,没写「失败长什么样」。 现在退出码只有两个值算正常,其余一律中止。
中途换了被测对象
一个测试在回归中途重建了数据库。 有个测试为了验证「重建是确定的」,会连着重建两次数据库——就地重建。于是那一轮回归的前四块测的是旧库,后四块测的是新库。前四块里有一整块全红,后四块全绿,看起来像是「只有那一块坏了」。判据:回归跑完先看工作区,应该和跑之前一样干净。
上一次实机测试留下的开关。 在游戏里实测时打开过一个开发者模式的静态开关,退出 Play 并不会把它复位。紧接着的回归里多红了几个存档相关的测试,看起来像存档回归了。多出来的失败,先想「上一步动过什么静态状态」,再去怀疑代码。
断言太弱
「是个物体」和「是那个物体」。 有个测试检查资源注册表里的每个角色都指向一个 prefab,断言是「解析出来是个游戏物体,并且属于某个 prefab」。注册表里有四条其实指向的是 prefab 里的子物体——国王那条指向的是一片胡子——它们照样满足这条断言,于是这个测试一直是绿的。直到我们想删掉 prefab 里几个看似没用的物体,回归才全红。之后断言收紧成「必须是 prefab 的根」。从那以后,写断言时我会问一句:满足这条断言的最坏情况长什么样? 如果那个最坏情况恰好就是要防的错误,这条断言就没写对。
只在文件内部找引用。 删 prefab 里的物体之前,我们扫了 prefab 文件内部所有对它的引用,确认没人用。可注册表是从文件外面指进来的,内部扫描根本看不见。
用被检查的东西去检查它自己。 机器人核对「词有没有进正确的笔记本页」时,刻意没有用游戏里决定归档的那个函数——两者一起错的时候,它会说「对」。
量的东西不对
从图片元数据推尺寸。 笔记本里词的背景图是被拉伸使用的,从元数据读出来的四档尺寸,和真实的词框完全对不上。
在界面排版之前量尺寸。 词刚生成出来的那一帧,量到的是预制体裸的 10 × 20,算法按这个尺寸摆,进游戏一看,八个词里三个出框、四个重叠。离线测试抓不到,因为测试里的尺寸是直接传进去的——错的不是算法,是喂进去的数。
字体「完整覆盖」。 游戏用静态字体图集,图集里没有的字会直接显示成空白。我们有个检查:字体是否覆盖了字符集里的所有字。它通过了。可字符集本身漏了一整类字段——一些教程文案存在字符串数组里,提取脚本只抓了另一种写法——于是「描述性前缀」的「性」字一直是空白。检查量的是「字体对字符集」,没量「字符集对实际文本」。
绿,是因为没人测
有一次要把一个旧分支上的工作拆进新的开发分支。图省事,直接按目录把整个文件取了过来。新分支在那之后对同名文件的改动——250 行——被悄悄覆盖了。类型检查、内容检查、回归,全部是绿的,因为被删掉的那些东西,本来就没有测试覆盖。
之后的做法是在旧工作真实的起点上做一次增量提交,再让 git 做三方合并;判据是:统计「新分支有、结果里却不见了」的行数,必须为零,或者每一行都能解释。
数对了,成分不对
项目里有一批重构前就存在的已知失败测试。回归时我们不看「有没有红的」,而看「红的是不是正好那些」。只比数量是不够的:曾经有两轮的失败总数都是 22,可一轮里有一个新失败、同时修好了一个旧的。所以要按名字逐条比。
可按名字比也有前提:名单本身得可信。有一次逐个程序集实跑,发现名单漏了四项——那四项早就红了,只是没人记。
留下来的两个习惯
反向探针。 一个新写的检查,在相信它的绿之前,先故意把被检查的东西改坏,看它会不会变红。不会变红的检查,绿了也没有意义。这个习惯后来几乎成了每次加检查的固定步骤,项目的提交记录里有四十多次提到它。
但反向探针也有自己的盲区:它只能证明已经写下的断言会响应,证明不了断言覆盖得够不够全。有一次做了十一处反向探针、全部抓到,复查时才发现有两条断言一开始根本没写。
写失败信号。 每写一个检查、一个监视,先问自己:如果它现在是错的,这个检查会不会出声? 不会,就把判据放宽到它会出声为止。
回头看
这些坑长得不一样,形状却很相似:检查本身是对的,只是它验的不是我以为的那样东西——同一样东西比了两遍、比的是旧版本、根本没跑、跑到一半换了对象、断言比意图弱、量错了尺寸、该测的地方没人测。
能防住它们的,不是更多的检查,而是对每一个检查多问一句:它现在到底在看什么?