16.2 自动化冒烟与截图比对
16.1 的类型门禁能挡住「API 写错」,单测能挡住「逻辑算错」。但还有一类问题它们都看不见:代码全对、测试全绿,构建出来的包却是白屏——可能是资源路径错、可能是首场景名写错、可能是某个组件在真机上抛异常。这类问题只有「真的把游戏跑起来看一眼」才能发现。
与站内 game/client/ 的测试文章的关系:那些文章多讲单元测试;本节讲的是端到端冒烟 + 视觉回归在 Cocos 工程里的落地方式——站内很少有人写「怎么在 CI 里自动打开一个小游戏并截图」。
冒烟(smoke test)的原则是「少而关键」:不追求覆盖所有玩法,只保证主路径能走通、首屏长得对。一局跑酷的主路径是「冷启动 → 主菜单 → 点开始 → 进关卡 → 跳一下」。跑通它,就等于确认了「这个包至少能用」。
16.2.1 冒烟要测的那一条主路径
1.1 的用例表里,UC-01「首次进入」就是冒烟该覆盖的主路径。把它拆成可断言的步骤:
| 步骤 | 断言 | 失败表现 |
|---|---|---|
| 打开页面 | 画布可见 | 白屏 |
| 引擎启动完成 | __BLOCK_DASH_READY__ 为 true | 卡在加载 |
| 首屏外观 | 截图与基线像素差 < 2% | UI 错位、贴图丢失 |
| 点一下屏幕 | 无异常抛出 | 输入无响应 |
第一步与第三步是视觉断言,第二步是状态断言,第四步是交互断言。四步都过,才叫「冒烟通过」。
冒烟和 16.1 的单测分工不同,别指望一个替代另一个:
| 维度 | 单元测试(16.1) | 冒烟(本节) |
|---|---|---|
| 测什么 | 纯逻辑的输入输出 | 整包能否跑起来 |
| 跑在哪 | Node | 浏览器 / 真机 |
| 速度 | 毫秒级 | 十秒级 |
| 覆盖 | 分支与边界 | 只覆盖主路径 |
| 挡什么 | 算错 | 白屏、崩、UI 错位 |
一句话:单测证明「逻辑对」,冒烟证明「包能用」。 两者都不能省,但优先级不同——冒烟先做,因为它挡的是「根本打不开」这种最严重的问题。
16.2.2 让构建产物能被冒烟识别
截图比对需要一个稳定的「准备好了」信号。下面这个组件在首帧绘制后,往 window 上打一个标记,Playwright 就能等它:
import { _decorator, Component, director, Director } from 'cc';
const { ccclass } = _decorator;
declare global {
interface Window {
__BLOCK_DASH_READY__?: boolean;
}
}
@ccclass('SmokeProbe')
export class SmokeProbe extends Component {
onLoad(): void {
director.once(Director.EVENT_AFTER_DRAW, this.markReady, this);
}
private markReady(): void {
if (typeof window !== 'undefined') {
window.__BLOCK_DASH_READY__ = true;
}
}
}
director.once(Director.EVENT_AFTER_DRAW, ...) 只触发一次——首帧画完就置位。这个标记必须挂在启动场景的常驻节点上,否则场景切换时组件被销毁,标记也没了。typeof window !== 'undefined' 的判断是为了让这段代码在原生端(没有 window)也不报错——同一份脚本要跑三端。
16.2.3 用 CLI 构建 H5 产物
冒烟测的是构建产物,不是编辑器预览,所以 CI 里要先跑一次构建。Cocos Creator 提供了命令行构建:
# 需本机已安装 Cocos Creator 3.8.8 编辑器(CI 上要自建 runner 或预装编辑器镜像)
/Applications/Cocos/Creator/3.8.8/CocosCreator.app/Contents/MacOS/CocosCreator \
--project "$PWD" \
--build "platform=web-mobile;debug=false;md5Cache=true"
# 产物落在 build/web-mobile/
ls build/web-mobile/index.html
三点提醒。第一,这条命令依赖编辑器二进制,GitHub 官方的 ubuntu runner 上没有它——要么用自托管 runner,要么在别处构建好、把 build/web-mobile/ 作为 artifact 传进冒烟 job。第二,platform=web-mobile 是 H5 移动端构建,与真机接近,比 web-desktop 更能暴露适配问题。第三,debug=false 关掉调试信息,产物更接近发布包。
构建完起一个静态服务,让 Playwright 能访问:
npx http-server build/web-mobile -p 8080 -c-1
# -c-1 关闭缓存,避免冒烟读到上一轮的旧文件
16.2.4 用 Playwright 驱动一局
Playwright 负责「打开页面、等信号、点一下、截图」。下面是一条完整的冒烟用例:
import { test, expect } from '@playwright/test';
test('冷启动到第一屏冒烟', async ({ page }) => {
await page.goto('http://127.0.0.1:8080/');
const canvas = page.locator('#GameCanvas');
await expect(canvas).toBeVisible({ timeout: 15000 });
await page.waitForFunction(
() => (window as unknown as { __BLOCK_DASH_READY__?: boolean }).__BLOCK_DASH_READY__ === true,
null,
{ timeout: 15000 },
);
await expect(page).toHaveScreenshot('first-screen.png', { maxDiffPixelRatio: 0.02 });
await page.mouse.click(480, 270);
await page.waitForTimeout(300);
await expect(canvas).toBeVisible();
});
#GameCanvas 是 Cocos H5 构建默认的画布 id。waitForFunction 等的是 16.2.2 打的标记——不要用固定 waitForTimeout 代替它,那会让冒烟在慢机器上随机失败。toHaveScreenshot 是 Playwright 的截图比对断言:首次运行生成基线图,之后每次运行拿新截图和基线比,像素差超过 maxDiffPixelRatio(这里 2%)就失败。
光有首屏还不够,主路径里「点一下能跳」也该被断言。给同一条用例追加一段,用状态变化而不是截图来验证交互:
import { test, expect } from '@playwright/test';
test('点击后角色进入跳跃状态', async ({ page }) => {
await page.goto('http://127.0.0.1:8080/');
await page.waitForFunction(
() => (window as unknown as { __BLOCK_DASH_READY__?: boolean }).__BLOCK_DASH_READY__ === true,
null,
{ timeout: 15000 },
);
const before = await page.evaluate(
() => (window as unknown as { __BLOCK_DASH_STATE__?: string }).__BLOCK_DASH_STATE__,
);
await page.mouse.click(480, 270);
await page.waitForFunction(
(prev) => (window as unknown as { __BLOCK_DASH_STATE__?: string }).__BLOCK_DASH_STATE__ !== prev,
before,
{ timeout: 3000 },
);
await expect(page.locator('#GameCanvas')).toBeVisible();
});
这里用第二个标记 __BLOCK_DASH_STATE__(由玩法脚本在状态切换时写入)来断言「点击确实产生了状态变化」。用状态断言代替截图断言,是交互测试比外观测试更稳的关键——状态是确定性的,像素不是。两段用例都过,才算「主路径能走通且能交互」。
16.2.5 截图比对的基线怎么管
截图比对的难点不在跑,而在基线图(baseline)怎么维护。基线图必须进版本库,且只能在「确认这次改动是预期的」时更新:
# 第一次运行:生成基线(把生成的图提交进仓库)
npx playwright test --update-snapshots
# 之后每次运行:与基线比对,不一致则失败
npx playwright test
# 确认某个改动是预期的(比如 UI 改版),手动更新基线并单独提交
npx playwright test --update-snapshots
这里有个纪律:更新基线必须是一次「有意的、单独的」提交,提交信息里写清「为什么改」。如果随手 --update-snapshots 就把基线覆盖了,视觉回归就形同虚设——因为它永远和当前状态一致。
16.2.6 在 CI 里怎么落
把构建、起服务、冒烟、归档串成一个 job:
name: smoke
on:
pull_request:
paths: ["assets/**", "settings/**"]
jobs:
smoke:
runs-on: [self-hosted, cocos] # 需要装有 Cocos Creator 3.8.8 的 runner
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm ci
- name: build web
run: ./scripts/build-web.sh # 封装 16.2.3 的 CLI 命令
- name: serve
run: npx http-server build/web-mobile -p 8080 -c-1 &
- name: smoke
run: npx playwright test
- uses: actions/upload-artifact@v4
if: failure()
with:
name: smoke-screenshots
path: test-results/
失败判定分两层:Playwright 用例失败(画布不可见、标记超时、截图差异超阈值)时进程退出码非 0,job 红;同时 if: failure() 把 test-results/ 目录(含实际截图、期望截图、差异图)上传成 artifact,reviewer 下载就能看到「到底是哪里不一样」。产物就是这个 artifact——没有它,光知道「截图对不上」没法排查。
--update-snapshots 这个开关绝不能出现在 CI 脚本里。一旦 CI 自动更新基线,视觉回归就退化成「每次都和上一轮比」,任何改动都会被它自己接受——门禁形同虚设。基线更新必须是人手动触发、单独提交的动作(16.2.5)。
16.2.7 冒烟覆盖不到的地方
冒烟在 H5 上跑通,不代表三端都通。三端各有盲区:
| 平台 | 冒烟怎么跑 | 盲区 |
|---|---|---|
| H5 | Playwright(本节这套) | 不覆盖小游戏宿主 API |
| 微信小游戏 | 微信开发者工具的自动化接口 | 真机性能仍需复测 |
| 原生 | 真机 / 模拟器 | CI 难自动化,多靠手动 |
所以本节这套只覆盖 H5 端。小游戏端可以用微信开发者工具的自动化跑一遍同样的主路径,原生端则往往只能保留「发布前手动过一遍 18.1 检查清单」这一步。不要因为 H5 冒烟绿了,就以为三端都安全——宿主环境的差异(wx.* API、原生 JSB)恰恰是冒烟在浏览器里碰不到的。
16.2.8 让冒烟稳定的三个技巧
冒烟最怕「随机红」,三次里红一次,团队就会开始忽略它。三条反波动措施:
- 等信号,不等时间:用
waitForFunction等__BLOCK_DASH_READY__,不用固定waitForTimeout。 - 关掉非确定性动画:截图前把粒子、飘字、背景滚动暂停或固定随机种子,否则每次截图都不同。
- 固定视口与设备倍率:Playwright 配置里写死
viewport: { width: 960, height: 540 }与deviceScaleFactor: 1,避免不同分辨率下布局漂移。
deviceScaleFactor 尤其重要:它一变,截图像素尺寸就变,比对必然失败。把它写进配置,而不是依赖运行环境默认值。
还有一个容易被忽略的波动源:字体渲染。不同操作系统、不同浏览器对同一字体的抗锯齿不同,同一段文字截出来的像素会有细微差异。对策是冒烟环境用同一套容器镜像(CI 里用固定的 Playwright Docker 镜像),本地与 CI 保持一致。如果实在无法统一,就把含文字的区域用 mask 参数遮掉,只比对图形区域。
这四条合起来是一句话:让每次运行的外部条件尽可能一致,把「不确定」挤到最小。 冒烟的价值不在于测得全,而在于测得稳——一个三天两头误报的冒烟,比没有冒烟更糟,因为它会训练团队忽略红灯。
16.2.9 工程视角的收口
这一节做完,工程里多了:assets/scripts/platform/SmokeProbe.ts(往 window 打就绪标记)、一份 playwright.config.ts、一组 tests/smoke.spec.ts 与基线截图目录、scripts/build-web.sh(封装 CLI 构建),以及一个 CI 的 smoke job。质量保障从「类型 + 逻辑」扩到了「真实运行」。
冒烟保证了「包能跑、首屏对」,但它只在 CI 里跑——真实用户那边出问题时,你不在场。用户遇到崩溃、卡死、某个机型黑屏时,如果没有上报,你只能靠「用户反馈」猜。下一节把崩溃上报与埋点接进来,让线上问题看得见。
阅读导航:上一节:16.1 逻辑单测与类型校验 · 下一节:16.3 崩溃上报与埋点 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。