<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>测试 on 蜂窝</title><link>https://yufenghuang.tech/tags/%E6%B5%8B%E8%AF%95/</link><description>Recent content in 测试 on 蜂窝</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><managingEditor>yufenghuang009@gmail.com (Evan Wong)</managingEditor><webMaster>yufenghuang009@gmail.com (Evan Wong)</webMaster><copyright>EvanWong</copyright><lastBuildDate>Mon, 12 Oct 2026 10:20:00 +0800</lastBuildDate><atom:link href="https://yufenghuang.tech/tags/%E6%B5%8B%E8%AF%95/index.xml" rel="self" type="application/rss+xml"/><item><title>HeirCut 开发手记（九）：看起来在验证，其实验的是别的东西</title><link>https://yufenghuang.tech/p/heircut-devlog-verification-traps/</link><pubDate>Mon, 12 Oct 2026 10:20:00 +0800</pubDate><author>yufenghuang009@gmail.com (Evan Wong)</author><guid>https://yufenghuang.tech/p/heircut-devlog-verification-traps/</guid><description>&lt;blockquote&gt;
&lt;p&gt;「HeirCut 开发手记」第九篇。游戏简介见&lt;a class="link" href="https://yufenghuang.tech/p/heircut-devlog-submit-guidance/" &gt;第一篇&lt;/a&gt;文末。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;做《宫廷理发师》的这一年里，最让我头疼的问题，往往不是哪个检查失败了——失败至少会指出哪里不对。最麻烦的是&lt;strong&gt;检查通过了，但它验的根本不是我以为的那样东西&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;具体是哪一次最头疼，我已经记不清了。但印象里不止一次是这样：排查时以为问题出在一个方向，查到最后才发现在另一个方向。代码里也因此留下了不少调试日志。&lt;/p&gt;
&lt;p&gt;回头翻记录，这样的坑我们踩了十几个。回头看，它们可以归成几种形状。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://yufenghuang.tech/p/heircut-devlog-verification-traps/trap-shapes.svg"
loading="lazy"
alt="验证陷阱的八种形状：同一样东西比了两遍、验的是旧版本、根本没跑、中途换了被测对象、断言比意图弱、量错了东西、绿是因为没人测、数对了成分不对"
&gt;&lt;/p&gt;
&lt;h2 id="同一样东西比了两遍"&gt;&lt;a href="#%e5%90%8c%e4%b8%80%e6%a0%b7%e4%b8%9c%e8%a5%bf%e6%af%94%e4%ba%86%e4%b8%a4%e9%81%8d" class="header-anchor"&gt;&lt;/a&gt;同一样东西比了两遍
&lt;/h2&gt;&lt;p&gt;为了给打包瘦身，我们想给一批贴图开压缩，写了个工具：分别导出压缩前、压缩后的图，逐像素比较。结果是&lt;strong&gt;每个像素都相同，最大差值为 0&lt;/strong&gt;。看起来像是「压缩几乎无损，可以放心全量开」。&lt;/p&gt;
&lt;p&gt;实际上压缩根本没生效。GPU 的块压缩格式要求图片的宽和高都是 4 的倍数，这批图经过最大尺寸缩放之后，落在了 2048 × 1153 这种尺寸上——差一个像素就压不了，Unity 也不报错，静默退回未压缩的格式。工具比的，是同一张图的两份拷贝。&lt;/p&gt;
&lt;p&gt;唯一的破绽是工具两次都打印了「格式 = RGBA32」。之后的判据是：&lt;strong&gt;比较压缩效果之前，先断言格式确实变了&lt;/strong&gt;，变了才比像素。&lt;/p&gt;
&lt;h2 id="验的是旧版本"&gt;&lt;a href="#%e9%aa%8c%e7%9a%84%e6%98%af%e6%97%a7%e7%89%88%e6%9c%ac" class="header-anchor"&gt;&lt;/a&gt;验的是旧版本
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;改了代码默认值，运行时还是老数。&lt;/strong&gt; 给一个已有组件新加一个可序列化字段时，Unity 会在第一次编译时，把当时的代码默认值烘进 prefab 的导入产物里。之后再改代码里的默认值，产物不会重新生成。我们为一个手感参数调了四轮，每一轮都在核对源码，可运行时读的一直是第一版的值——新加的日志都打出来了，唯独这个字段是旧的。之后调参期间一律用常量，定下来再暴露成字段。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;测试跑的是上一轮编译的程序集。&lt;/strong&gt; 命令行模式下改完测试直接跑，Unity 有时会先用磁盘上旧的程序集跑完，退出时才重新编译。症状是失败信息里的期望值在源码里&lt;strong&gt;根本搜不到&lt;/strong&gt;。判据也就是这个：拿失败信息里的字面量去搜源码，搜不到就是旧程序集，重跑。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;离线检查读的是源头，游戏读的是成品。&lt;/strong&gt; 剧情内容要「重建」成数据库，游戏才读得到。有一次内容格式升级了，离线校验直接读源文件，全绿；可仓库里那份重建好的数据库还是旧格式，几十个依赖它的测试一启动就报错。离线通过，不等于成品已经更新。&lt;/p&gt;
&lt;h2 id="根本没跑"&gt;&lt;a href="#%e6%a0%b9%e6%9c%ac%e6%b2%a1%e8%b7%91" class="header-anchor"&gt;&lt;/a&gt;根本没跑
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;新测试从来没被执行过。&lt;/strong&gt; 有一阵我们的回归脚本是按测试类的名字过滤、分块跑的。新加的一个测试类，名字不在任何一块的过滤条件里，于是整轮回归一次都没跑到它——而总数看起来毫无异常。直到有一次整程序集跑，才发现总数是 169，不是 166。判据变成：&lt;strong&gt;加了测试，总数就该涨；没涨，就是没跑到。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;过滤条件写成了字符串 &amp;ldquo;null&amp;rdquo;。&lt;/strong&gt; 测试工具要求某个过滤参数必须出现，传了字符串 &lt;code&gt;&amp;quot;null&amp;quot;&lt;/code&gt;，它就去找一个叫 null 的测试，结果是「成功，共 0 项」。看起来全绿，其实一项没跑。之后每次都核对总数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;监视脚本只认成功信号。&lt;/strong&gt; 等回归跑完的监视脚本，只看「这一块跑完了」的标记有没有出现。有一次编辑器没关，八块测试全部因为「已有一个 Unity 实例在运行」而崩溃退出，监视脚本却把八次崩溃报成了八次跑完。在这之前，同一个脚本还死等过一个已经被删掉的文件，空转了将近四个小时。两次的形状一样：&lt;strong&gt;只写了「成功长什么样」，没写「失败长什么样」。&lt;/strong&gt; 现在退出码只有两个值算正常，其余一律中止。&lt;/p&gt;
&lt;h2 id="中途换了被测对象"&gt;&lt;a href="#%e4%b8%ad%e9%80%94%e6%8d%a2%e4%ba%86%e8%a2%ab%e6%b5%8b%e5%af%b9%e8%b1%a1" class="header-anchor"&gt;&lt;/a&gt;中途换了被测对象
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;一个测试在回归中途重建了数据库。&lt;/strong&gt; 有个测试为了验证「重建是确定的」，会连着重建两次数据库——就地重建。于是那一轮回归的前四块测的是旧库，后四块测的是新库。前四块里有一整块全红，后四块全绿，看起来像是「只有那一块坏了」。判据：&lt;strong&gt;回归跑完先看工作区，应该和跑之前一样干净。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;上一次实机测试留下的开关。&lt;/strong&gt; 在游戏里实测时打开过一个开发者模式的静态开关，退出 Play 并不会把它复位。紧接着的回归里多红了几个存档相关的测试，看起来像存档回归了。多出来的失败，先想「上一步动过什么静态状态」，再去怀疑代码。&lt;/p&gt;
&lt;h2 id="断言太弱"&gt;&lt;a href="#%e6%96%ad%e8%a8%80%e5%a4%aa%e5%bc%b1" class="header-anchor"&gt;&lt;/a&gt;断言太弱
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;「是个物体」和「是那个物体」。&lt;/strong&gt; 有个测试检查资源注册表里的每个角色都指向一个 prefab，断言是「解析出来是个游戏物体，并且属于某个 prefab」。注册表里有四条其实指向的是 prefab 里的&lt;strong&gt;子物体&lt;/strong&gt;——国王那条指向的是一片胡子——它们照样满足这条断言，于是这个测试一直是绿的。直到我们想删掉 prefab 里几个看似没用的物体，回归才全红。之后断言收紧成「必须是 prefab 的根」。从那以后，写断言时我会问一句：&lt;strong&gt;满足这条断言的最坏情况长什么样？&lt;/strong&gt; 如果那个最坏情况恰好就是要防的错误，这条断言就没写对。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;只在文件内部找引用。&lt;/strong&gt; 删 prefab 里的物体之前，我们扫了 prefab 文件内部所有对它的引用，确认没人用。可注册表是从文件&lt;strong&gt;外面&lt;/strong&gt;指进来的，内部扫描根本看不见。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;用被检查的东西去检查它自己。&lt;/strong&gt; 机器人核对「词有没有进正确的笔记本页」时，刻意没有用游戏里决定归档的那个函数——两者一起错的时候，它会说「对」。&lt;/p&gt;
&lt;h2 id="量的东西不对"&gt;&lt;a href="#%e9%87%8f%e7%9a%84%e4%b8%9c%e8%a5%bf%e4%b8%8d%e5%af%b9" class="header-anchor"&gt;&lt;/a&gt;量的东西不对
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;从图片元数据推尺寸。&lt;/strong&gt; 笔记本里词的背景图是被拉伸使用的，从元数据读出来的四档尺寸，和真实的词框完全对不上。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;在界面排版之前量尺寸。&lt;/strong&gt; 词刚生成出来的那一帧，量到的是预制体裸的 10 × 20，算法按这个尺寸摆，进游戏一看，八个词里三个出框、四个重叠。离线测试抓不到，因为测试里的尺寸是直接传进去的——错的不是算法，是喂进去的数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;字体「完整覆盖」。&lt;/strong&gt; 游戏用静态字体图集，图集里没有的字会直接显示成空白。我们有个检查：字体是否覆盖了字符集里的所有字。它通过了。可字符集本身漏了一整类字段——一些教程文案存在字符串数组里，提取脚本只抓了另一种写法——于是「描述性前缀」的「性」字一直是空白。检查量的是「字体对字符集」，没量「字符集对实际文本」。&lt;/p&gt;
&lt;h2 id="绿是因为没人测"&gt;&lt;a href="#%e7%bb%bf%e6%98%af%e5%9b%a0%e4%b8%ba%e6%b2%a1%e4%ba%ba%e6%b5%8b" class="header-anchor"&gt;&lt;/a&gt;绿，是因为没人测
&lt;/h2&gt;&lt;p&gt;有一次要把一个旧分支上的工作拆进新的开发分支。图省事，直接按目录把整个文件取了过来。新分支在那之后对同名文件的改动——250 行——被悄悄覆盖了。类型检查、内容检查、回归，全部是绿的，因为被删掉的那些东西，本来就没有测试覆盖。&lt;/p&gt;
&lt;p&gt;之后的做法是在旧工作真实的起点上做一次增量提交，再让 git 做三方合并；判据是：&lt;strong&gt;统计「新分支有、结果里却不见了」的行数，必须为零，或者每一行都能解释。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="数对了成分不对"&gt;&lt;a href="#%e6%95%b0%e5%af%b9%e4%ba%86%e6%88%90%e5%88%86%e4%b8%8d%e5%af%b9" class="header-anchor"&gt;&lt;/a&gt;数对了，成分不对
&lt;/h2&gt;&lt;p&gt;项目里有一批重构前就存在的已知失败测试。回归时我们不看「有没有红的」，而看「红的是不是正好那些」。只比数量是不够的：曾经有两轮的失败总数都是 22，可一轮里有一个新失败、同时修好了一个旧的。所以要&lt;strong&gt;按名字逐条比&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;可按名字比也有前提：名单本身得可信。有一次逐个程序集实跑，发现名单漏了四项——那四项早就红了，只是没人记。&lt;/p&gt;
&lt;h2 id="留下来的两个习惯"&gt;&lt;a href="#%e7%95%99%e4%b8%8b%e6%9d%a5%e7%9a%84%e4%b8%a4%e4%b8%aa%e4%b9%a0%e6%83%af" class="header-anchor"&gt;&lt;/a&gt;留下来的两个习惯
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;反向探针。&lt;/strong&gt; 一个新写的检查，在相信它的绿之前，先故意把被检查的东西改坏，看它会不会变红。不会变红的检查，绿了也没有意义。这个习惯后来几乎成了每次加检查的固定步骤，项目的提交记录里有四十多次提到它。&lt;/p&gt;
&lt;p&gt;但反向探针也有自己的盲区：它只能证明&lt;strong&gt;已经写下的断言会响应&lt;/strong&gt;，证明不了断言覆盖得够不够全。有一次做了十一处反向探针、全部抓到，复查时才发现有两条断言一开始根本没写。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;写失败信号。&lt;/strong&gt; 每写一个检查、一个监视，先问自己：&lt;strong&gt;如果它现在是错的，这个检查会不会出声？&lt;/strong&gt; 不会，就把判据放宽到它会出声为止。&lt;/p&gt;
&lt;h2 id="回头看"&gt;&lt;a href="#%e5%9b%9e%e5%a4%b4%e7%9c%8b" class="header-anchor"&gt;&lt;/a&gt;回头看
&lt;/h2&gt;&lt;p&gt;这些坑长得不一样，形状却很相似：检查本身是对的，只是它验的不是我以为的那样东西——同一样东西比了两遍、比的是旧版本、根本没跑、跑到一半换了对象、断言比意图弱、量错了尺寸、该测的地方没人测。&lt;/p&gt;
&lt;p&gt;能防住它们的，不是更多的检查，而是对每一个检查多问一句：&lt;strong&gt;它现在到底在看什么？&lt;/strong&gt;&lt;/p&gt;</description></item></channel></rss>