尧图精选

从零复刻《超级动物自走棋》游戏(4):没报错 ≠ 通过了

🕒 发布时间:2026/9/26 17:24:25 📁 来源:尧图网络
摘要本文记录了一次自动化测试踩坑经历测试脚本崩溃后不会打印结果行导致统计总数悄悄变少而毫无察觉。作者总结了四个隐蔽问题——崩溃脚本从统计中消失、断言函数签名过窄导致框架自身崩溃、写死的期望值造成假失败、以及没输出与崩溃难以区分并给出了三条自检规则统计脚本数量、用 try/catch 包裹断言、期望值现算不写死。最后分享了用假 DOM 测试界面的实践。系列说明前三篇讲了技术选型、数据考证和重复实现这一篇讲测试。核心是一句话没输出报错和测试通过了是两件完全不同的事。我在这上面栽过一次而且栽得很隐蔽 —— 测试总数悄悄变少了我一开始没发现。全程没有美化翻车的地方也照写。 边玩边读 Super Auto Pets · 单机版纯网页版点开就能玩——不用下载、不用注册、不用登录。一、先看一个数字写这篇的时候我手上的自动化测试是这样测试脚本27 个断言707 条失败0跑一遍大概十几秒覆盖战斗引擎、商店经济、三种模式的对局流程、联机协议、图鉴渲染。输出大概长这样截取一部分bugfix_browser.js 10 通过 / 0 失败 classic_battle_test.js 11 通过 / 0 失败 codex.js 24 通过 / 0 失败 factions_test.js 42 通过 / 0 失败 online_e2e.js 42 通过 / 0 失败 star13_test.js 45 通过 / 0 失败 test_battle.js 18 通过 / 0 失败 test_game.js 63 通过 / 0 失败 ...共 27 个脚本 ​ 合计: 707 通过 / 0 失败 这个规模不是一开始就有的。它是被 bug 逼出来的—— 每修一个 bug我就想怎么让它别再回来于是多几条断言。而这篇要讲的是我在测试本身上踩的坑。二、这次事故测试总数悄悄变少了每个测试脚本的结尾都是这两行console.log(\n 结果: ${pass} 通过 / ${fail} 失败 ); process.exit(fail 0 ? 1 : 0);我的跑全部测试命令做的是很简单的一件事把所有脚本跑一遍把X 通过 / Y 失败里的数字加起来。然后我盯着那个合计数字看它是不是在涨。合计数在涨 我在进步。直到有一次我加了一堆断言重跑看着总数……总数变少了。不是失败变多是总数本身少了。就好像凭空蒸发了几十条。三、原因崩溃的脚本会从总数里彻底消失如果你仔细看上面那个统计逻辑会发现它有个致命缺陷它是靠能不能匹配到结果行来收集数据的。而一个崩溃的脚本根本不会打印结果行。于是情况输出里有结果行吗对总数的影响脚本跑完100 条断言2 条失败✅ 有「100 通过 / 2 失败」总计 100 通过、2 失败 →你会看到失败脚本跑到第 30 行崩了❌没有总计什么都不加→你什么都不会看到注意第二行它不报错、不算失败、不计入任何数字。它只是不存在了。在那个总量几百条的池子里少几十条根本看不出来。这是我设计得最糟糕的一个东西我的测试统计把崩溃这种最严重的失败表现成了无事发生。修正后的统计逻辑多了一条 不但要统计多少条通过、多少条失败 还要统计有几个脚本报了结果。前者告诉我测试好不好后者告诉我测试有没有在跑。这条改完之后只要有脚本没报结果合计行下面就会直接列出它的名字而不是安静地少一块。四、第二个问题测试框架自己会崩上面那次崩溃的元凶是我自己写的断言函数。最早它是这样的function check(label, value) { const r value(); // 把传入的东西当函数调用 if (r false) { fail; console.log( FAIL label); } else { pass; console.log( PASS label); } }它是为惰性求值设计的传一个函数进去如果这个函数内部抛异常比如访问了不存在的字段就能被断言捕获而不是让整个脚本挂掉。设计意图没问题。问题是签名太窄—— 它只接受函数。于是当我或者 AI顺手写了这样一句check(分页标签数量 6 个, tabs.length 6);tabs.length 6是个布尔值check会去调用它 ——true()。TypeError: value is not a function。整个脚本当场挂掉。而且因为前面那节说的原因它挂掉之后我的总计数字又悄悄少了几条。修法function check(label, value) { try { const r (typeof value function) ? value() : value; if (r false) { fail; console.log( FAIL label); } else { pass; console.log( PASS label); } } catch (e) { fail; console.log( FAIL label - e.message); } }两处改动typeof判断是函数就调用不是就直接用 —— 两种写法都支持try / catch断言抛异常应该算这条断言失败而不是整个测试文件崩掉第 2 点是关键。它把失败的爆炸半径从一个文件缩小到了一条断言。这其实是个通用的道理测试代码是这个项目里最不应该崩溃的部分。测试崩了你连什么坏了都不知道 —— 它比被测代码崩了更麻烦。五、第三个问题写死的数字会变成假失败这个问题比前两个更烦因为它会骗你。我在图鉴测试里最初是这么写的check(宠物分页 138 只, api.codexTabCount(pack:star) api.codexTabCount(pack:turtle) 138);看起来很合理。但每加一只宠物这条断言就失败一次。失败之后我就得去查 —— 是图鉴渲染坏了还是分页逻辑错了查半天最后发现什么都没坏只是我加了一只宠物而这里写死了。这种失败比漏测更有害它浪费你的时间还会训练你忽略红色。当一条测试长期假失败你对红色的敏感度就下降了真正的问题就更容易溜过去。修法期望值要现算不要写死/* ⚠️ 数量不能写死 —— 每加一只宠物都会假失败。从 PETS 现算。 */ const wantPet Object.keys(api.PETS).filter(k !api.PETS[k].token api.PETS[k].tier 1).length; check(两个包分页的宠物加起来 wantPet 只, tabs.filter(t t.id.indexOf(pack:) 0) .reduce((n, t) n api.codexTabCount(t.id), 0) wantPet);注意这里检查的东西变了检查的是加一只宠物会怎样写死数字「图鉴里一共 138 只」❌ 假失败现算期望值「图鉴里显示的数量 数据里定义的数量」✅ 依然正确这就是上一篇提到过的一致性断言它不关心具体数字它关心两个地方对不对得上。同一个文件里还有一条一模一样的教训/* ⚠️ 类别件数不能写死 —— 改一个遗物的 tag 就会假失败 * 「急救包」从「战斗」挪到「成长」时就是这样。按 RELICS 现算。 */我把一个遗物从战斗类别挪到成长类别结果测试红了 ——不是游戏坏了是测试记错了。六、第四个问题崩掉的脚本和没输出长得一模一样有一次我需要查一个玩家反馈宠物都死了尸体还在打。我先在引擎层验证了一遍跑了 3000 场战斗分析日志里有没有已阵亡的宠物还在攻击 / 掉血 / 被加属性的情况 —— 0 处。引擎是干净的。所以问题只可能在回放层也就是显示。于是我写了一个专门的脚本去量化尸体在视图里停留多久跑完看结果 ——没有任何输出。我的第一反应是哦崩了。然后就去找崩溃原因。找了半天发现它根本没崩它只是没有按X 通过 / Y 失败的格式打印。它老老实实跑完了、检查完了只是最后用了一种我自己都不认识的输出格式。这又是同一个坑的另一个版本我的判断依据是有没有那行结果而不是脚本到底跑成什么样。修法很土但很有效给它补上和别的脚本一样的check()和结尾输出。console.log(\n pass 通过 / fail 失败); process.exit(fail ? 1 : 0);然后它立刻开始正常报数了现在的输出是2 通过 / 0 失败顺便说这个脚本查出来的结论也很有意思尸体的确会停留 1 帧—— 因为回放器的顺序是「先滤掉上一帧标记的死亡 → 再应用一条事件 → 再重绘」所以应用完阵亡事件到下一帧把它滤掉之间它会在画面上多留一帧。但这一帧是有意的那是阵亡动画而且量化下来最多就是 1 步不会更久。所以真正要修的不是引擎、也不是回放逻辑而是显示时机—— 这个结论只有在能量化的情况下才敢下。七、给测试加自检三条规则这一轮之后我给自己定了三条1. 总数必须能对上跑完之后有几个脚本报了结果要和我预期跑几个脚本一致。不一致就说明有东西静默消失了 —— 这是唯一能抓住崩溃的办法。2. 断言不能崩断言函数要用try / catch包住单条断言失败不能炸掉整个文件断言函数要能接受函数和值两种形式别让调用方去猜⚠️这一条我犯的错特别典型我给被测代码写了很多测试却从来没测过测试框架自己3. 期望值现算不写死凡是会随开发变化的数字都不要写进断言里。更好的做法是断言两个来源一致而不是断言等于某个值。八、那界面怎么测引擎层的测试好写喂一个队伍组合、跑一场战斗、检查事件日志。但界面就没那么好办了——总不能在测试里开个浏览器。我的做法是造一个假的 DOMfunction makeEl(tag) { const e { style: {}, dataset: {}, children: [], classList: { add(){}, remove(){}, toggle(){}, contains(){ return false; } }, appendChild(c) { this.children.push(c); return c; }, ... }; return e; } global.document { createElement: makeEl, querySelector: ..., ... };然后把真实的界面代码加载进来跑。这样能测到的东西比我预想的多对手行不显示那个 bug—— 就是靠打印computedStyle.display才发现的光看 DOM 结构它是正确的道具图标全是西瓜—— 断言每种食物在商店里显示的 emoji都必须和 emoji 表一致商店卡片上显示的价格是不是真的走了折扣函数另外我还留了三个真正在浏览器里跑的测试页专门验证渲染出来长什么样有些东西 DOM 桩模拟不了比如 CSS——包括图鉴的每个分页、每种宠物/道具的图标、以及羁绊卡片的渲染。九、小结这段经历让我把一句话记死了没报错 和 通过了中间隔着整整一类失败。具体到三条崩溃是最严重的失败但它最容易表现得像无事发生。如果你只统计通过/失败崩溃会从统计里消失。一定要单独统计有几个东西跑了。测试框架自己也需要被信任。一个只有函数一种签名、还没try/catch的断言函数能把整份测试变成摆设。写死的期望值会训练你忽略红色。假失败比漏测更有害因为它消耗的是你对告警的信任。 说说你的想法如果你也在做类似的东西或者去玩了一下发现了问题——欢迎在评论区告诉我每一条我都会认真看。没思路的小伙伴给大家四类参考类型你可以怎么说为什么对我有用报 bug我打完一局有只宠物血量是负数还站在场上最好带上你当时在干什么我复现会快很多⚖️数值/平衡小兽这个羁绊明显比别的强我有时候测不出来因为我有点菜没打过人机我觉得是我设置的人机太强了不是我太弱玩不明白这个技能我读了三遍还是没懂这是我的文案有问题算 bug新点子能不能加个 XX 模式XX宠物XX道具XX可以美化一下吗等等等等好玩的话我也想弄出来玩不一定要说得专业。感觉这个太强了这里怪怪的——这种模糊的直觉往往比精确的分析更有用因为我自己一个人好多实际游玩bug一时半会碰不到。下一篇写什么《把感觉太强变成数据》这一篇讲平衡怎么让这个羁绊好像有点超模这种模糊的感觉变成一个能反复测量、能验证是否修好了的数字。包括我自己犯的一个方法论错误控制变量没做对、以及一个我特意加的自检——用来证明我的测试脚本真的能测出问题而不是一直在报平衡良好。在线版还在持续迭代中——你点进去玩到的可能比我写这篇时又新了一些。如果发现哪里和文章对不上多半是我后来改了。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →