ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

GDevelop 视觉猴子测试(Visual Monkey Tests)完全指南:在真实浏览器中像用户一样操纵编辑器

GDevelop 视觉猴子测试(Visual Monkey Tests)完全指南:在真实浏览器中像用户一样操纵编辑器 GDevelop 视觉猴子测试Visual Monkey Tests完全指南在真实浏览器中像用户一样操纵编辑器【免费下载链接】GDevelop Open-source, cross-platform 2D/3D/multiplayer game engine designed for everyone.项目地址: https://gitcode.com/GitHub_Trending/gd/GDevelop视觉猴子测试Visual Monkey Tests是 GDevelop 编辑器测试体系中最贴近真实用户的一层它在真实浏览器中像用户一样点击、输入、拖拽并在每一次操作之后立即验证编辑器没有被破坏。本文基于 newIDE/visual-tests/README.md 展开结合visual-tests目录下的实际源码run.js、Runner.js、PageDriver.js、SpriteEditor.js 等深入讲解其架构与原理。读完本文你将掌握如何在本仓库中运行这两套测试、如何为一个编辑器编写脚本化操作与随机 monkey 会话、以及这套测试在 CI 中按变更裁剪运行的策略。什么是视觉猴子测试它能抓住单元测试看不到的缺陷单元测试天然是局部的它把一个函数或一个组件隔离出来验证。但编辑器的很多缺陷只会在所有代码一起运行时才会暴露界面上显示了项目里并不存在的内容一次操作作用在了错误的元素上例如拖拽把动画排错了位置一个未捕获的错误直接把整个编辑器打挂。视觉猴子测试的做法是像用户一样操纵编辑器——真实浏览器、真实点击、真实拖拽——并且在每一次单独的操作之后检查一切是否仍然正常。正如 README 所说几十次随机操作就能在几秒钟内发现这类问题而这些正是单元测试看不见的盲区。从实现上看像用户一样并非空话。PageDriver.js 中的dragFromTo使用 Puppeteer 的page.mouse发送真实鼠标事件先移动到起点按下再移动超过touchSlop10px以触发拖拽启动然后分步移动到目标点并松手——因为拖拽后端如 react-dnd只响应真实输入事件。同样地watchPageErrors同时监听pageerror事件和 React 通过 console 上报的错误确保任何未捕获异常都不会被放过。两套测试套件storybook-tests 与 editor-testsvisual-tests下有两个目录对应两种互补的测试套件storybook-tests/editor-tests/运行于某个编辑器的 Storybook stories真实打包的应用打开一个真实的游戏检查内容界面显示的内容 vs. 项目实际包含的内容编辑器还在、没有抛出任何异常用途对单个编辑器的精确测试冒烟测试没有被严重破坏为什么是这种分工Storybook 套件能检查得多得多因为一个 story 可以暴露被编辑的对象本身例如通过window.spriteEditorManipulations.readAnimations()读取对象真实的动画数据见 SpriteEditor.js。而 editor 套件运行的是真实应用只能观察页面本身凡是经过菜单桌面应用的原生菜单、文件选择器或外部编辑器窗口的操作都留给 Storybook 套件去测——因为原生菜单和系统文件对话框无法被测试驱动。在 run.js 中两套套件被统一描述为SUITES对象每个套件有自己的测试目录、运行函数和一句话描述这正是后续所有命令行选项统一的入口。环境准备与运行安装依赖cd newIDE/visual-tests npm install # 同时会下载用于运行的 Chrome依赖本身很轻量见 package.jsonpuppeteer驱动浏览器、minimist解析命令行参数、adm-zip解压便携版构建和serve-handler本地提供 Storybook 静态文件。常用运行命令node run.js --list node run.js --suitestorybook # 需要时先构建 Storybook然后运行 node run.js --suiteeditor # 下载最新的便携版构建 node run.js --suiteall--list只列出测试名称不做任何运行每个测试名后还会标注它是脚本化步骤steps还是随机会话monkey以及各自的操作数量这段信息来自 run.js 的describeTest。--suite支持逗号分隔或all默认值。命令行选项详解选项说明--testpart of a name只运行名称包含该子串的测试--headful显示浏览器窗口用于观察测试在做什么--verbose记录随机会话中的每一次操作--storybook-urlurl使用一个已在运行的 Storybook而不是重新构建--rebuild-storybook即使已构建也强制重建 Storybook--gdevelop-zippath使用这个便携版构建而不是下载一个--gdevelop-branchbranch下载该分支最新的便携版构建默认master--editor-monkey-stepsn真实应用上随机会话的操作次数默认 30--artifacts-dirpath截图和日志的存放目录默认artifacts--chrome-pathpath指定用于运行的 Chrome--only-changed --base-refref只运行与变更相关的测试--list-names,--tests-filepath列出 / 精确运行这些测试用于在 CI 的并行容器间拆分--junit-pathpath将结果写成 JUnit 格式这些选项在 run.js 中被minimist解析并转换为运行选项。有几点值得注意默认值storybook 端口默认9010--gdevelop-branch默认mastereditor 套件的 monkey 步数默认30工作目录默认./work--artifacts-dir会追加套件名每个套件的截图与日志存放在artifacts-dir/suiteName/下日志文件名为suiteName-visual-tests.log--tests-file与--list-names是配套的前者读取一个每行一个测试名的文件后者按行打印当前过滤出的所有测试名二者配合即可把测试集分发给多个 CI 容器并行执行--only-changed必须配合--base-ref它通过git diff --name-only base-ref...HEAD计算变更文件见 ChangedFiles.js默认基准是origin/master。package.json 还提供了几个便捷脚本npm run list、npm run test等价于--suiteall、npm run test-storybook和npm run test-editor。开发时的快速反馈循环写测试时最快的反馈方式是另开一个终端先跑npm run storybook然后在visual-tests目录下执行node run.js --storybook-urlhttp://localhost:9009 --headful --testname这样既跳过了 Storybook 的构建步骤又能用--headful亲眼看到测试在页面上的每一步操作。CI 中的运行策略按变更裁剪测试在分支上CI 只运行与本次变更相关的测试如果没有相关测试就什么都不构建而在master上则运行全部测试——因为任何一处的改动都可能破坏一个编辑器。若在流水线上显式触发run-all-visual-tests则会强制分支上也全量运行。相关是如何定义的每个 helper 在paths字段中声明它的测试所监视的源码路径见 SpriteEditor.js。ChangedFiles.js 的filterTestsForChangedFiles实现了一套明确的规则变更文件列表无法取得、为空或测试自身newIDE/visual-tests/下的文件发生了变更 → 运行全部测试否则只运行监视路径与变更文件有前缀匹配的测试没有声明paths的测试如通过 helper 间接获得路径的 editor 测试总是运行。两套套件都遵循这套规则但 editor 套件有一个差异在分支上editor 套件运行的是master最新的便携版构建分支本身不构建应用。也就是说分支上跑的是分支的测试 master 的应用分支对应用自身的改动由master上紧接着build-linux之后运行的同一套件来测试。另外测试发现但不是本套测试职责范围内要守护的问题可以登记在 lib/KnownIssues.js 中。它会被响亮地报告写入日志和摘要但不会让本次运行失败——这样一条分支就不会被它并未引入的问题挡住。目前登记的一个已知问题示例是 react-dnd 的Expected sourceIds to be registered异常Runner.js 中的reportStepResult会先匹配已知问题再决定判失败还是仅记录。已知问题一经修复就应立即从清单中移除这样问题复发时测试会重新失败。编写测试从脚本化步骤到随机猴子会话一个测试文件导出一个测试对象数组。Storybook 测试由一组操作步骤steps组成或者是一个用种子seeds保证可复现的随机会话monkeyconst spriteEditor require(../helpers/SpriteEditor); module.exports [ { name: sprite-editor/delete-animations, helper: spriteEditor, story: objecteditor-spriteeditormanipulations--manipulations, steps: [ [selectFrames, { row: 1, frames: [0, 1] }], [deleteAnimation, { row: 0 }], ], }, { name: sprite-editor/monkey, helper: spriteEditor, story: objecteditor-spriteeditormanipulations--manipulations, monkey: { seeds: [1, 2], steps: 60 }, }, ];注意两个字段的含义steps是[操作名, 参数]的二元组数组。操作名必须存在于 helper 的actions中不带参数的步骤如[addAnimation]在运行时由操作自带的pick决定是否适用与如何取值monkey的seeds数组决定随机会话的条数每个种子一条独立可复现的会话steps是每条会话的操作步数。例如monkey: { seeds: [1, 2], steps: 60 }会运行 2 条各 60 步的随机会话且每次运行结果完全一致——这是猴子可复现的关键。种子驱动的随机数生成器一个线性同余生成器实现在 Runner.js 的makeRandom中。Monkey 随机操作是如何随机的随机并不是均匀分布。Runner.js 的runMonkey依据 helper 声明的monkeyWeights权重表把操作名展开成加权列表再按权重抽样例如 SpriteEditor.js 中importImagesFromPlaceholder权重为 8高频而openPreview权重为 1低频。每一步操作之前还会调用closeAnyOverlay尝试关掉任何打开的菜单或对话框保证编辑器处于可用状态。--verbose或每 10 步会输出一次进度日志便于追踪。真实场景storybook-tests/sprite-editor.js中还组合了多个不同 story 的 monkey 会话序列化对象上的、多动画的、多帧的、锁定动画列表的、空对象的——每个会话覆盖一种边界状态例如序列化对象上的 monkey 会在每次变更后重新序列化使被释放的内存立即被复用从而暴露悬垂指针类问题。Editor 测试真实应用中的冒烟测试Editor 测试在真实打包的应用中打开一个真实示例游戏然后驱动它复用的是同一套操作机制{ name: editor/add-a-behavior-from-the-store, example: platformer, // 从 GDevelop-examples 稀疏克隆 helpers: [spriteEditor], // 要安装的页面辅助可选 run: async ({ page, reporter, screenshot, runSteps, runMonkey }) { ... }, }关键字段example指定要打开的示例游戏 slug如platformer由 lib/RealEditor.js 负责获取项目run测试主体接收一组已绑定的工具对象——page当前页面、reporter日志、screenshot截图、runSteps与runMonkey与 Storybook 套件相同的运行器见 lib/EditorSuite.js以及pageErrors、version、projectPath等。一个典型的 editor 测试见 editor-tests/behaviors.js会打开对象编辑器 → 切换到 Behaviors 标签 → 在扩展商店中搜索 Fire bullet → 添加 Fire bullets 行为此时扩展被下载并安装→ 对比前后行为列表确认新行为确实出现。这类测试虽然不如 Storybook 精确真实应用无法读取被编辑对象的内部数据但它验证的是整条真实链路打包产物、示例项目加载、扩展商店下载安装、界面渲染全部真实发生。扩展测试到编辑器的其他部分helper 架构整个测试体系的分层非常清晰通用部分在lib/驱动页面、运行与校验操作、两套套件、结果报告它们不感知任何特定编辑器而每个编辑器由helpers/中的一个helper描述helper 是唯一认识该编辑器的地方。helper 的组成README 原表installPageHelpers运行在页面内找到编辑器的控件并读取它显示的内容。注册一个 resolver使其目标某一行、某一帧……可以像通用目标一样使用{ button: Apply }、{ selector: #id }、{ menuItem: ... }、{ tab: Behaviors }actions所有操作以及 monkey 如何挑选它们pick、操作后必须满足什么expect由checkExpectation校验describe/check编辑器显示了什么以及它是否与项目匹配snapshot可选操作可能改变什么用于区分操作没起作用和操作成功否则用describe配describeEffect记录变化stepChecks可选编辑器的不变式每个不变式在每次被标记了其名称的操作之后被检查如 Sprite 编辑器中的keepsTheFramessummarize可选一行描述当前显示了什么在 story 打开时记录到日志paths其测试监视的源码路径只有这些路径发生变化时才运行相关测试helpers/SpriteEditor.js 是完整的参考实现helpers/ObjectsList.js、helpers/BehaviorsEditor.js 和 helpers/PropertiesPanel.js 是更小的例子供 editor 测试用来触达和操纵应用的其他部分。从源码看一次操作的完整执行链Runner.js 的runStep是这套体系的核心它精确地实现了每一步都检查的承诺。一次操作[deleteAnimation, { row: 0 }]的执行流程是调用helper.describe记录操作前的状态并用action.pick(stateBefore, random)决定参数无参数时调用action.describe(stepArgs)生成人类可读的操作描述若操作声明了expect先算出预期的精确结果执行action.run(page, stepArgs)如果操作本身抛异常立即记为失败检查页面错误队列一旦有未捕获错误标记为crashed编辑器被打挂调用helper.check比对界面显示的内容 vs. 项目实际包含的内容对比操作前后的snapshotJSON.stringify比较如果没有任何变化而操作声明了mustChangeTheObject则判定对象没有被改变——这保证一个测试不可能在没有真正执行任何操作的情况下通过若操作声明了expect调用checkExpectation校验精确结果再对操作标记过的每个stepChecks不变式逐一校验如 SpriteEditor.js 中的clearsTheFrameSelection——动画变更后帧选择必须被清空因为选择按索引引用帧动画一变索引就会指向别的帧。脚本化操作runSteps在第一次失败或命中已知问题时即中断而对标记为mayHaveNoEffect的拖拽操作它还会统计拖了但没有移动任何东西的次数——如果所有拖拽都没有产生任何效果运行会失败none of the drag and drops moved anything防止拖拽类操作形同虚设。通用操作与真实输入不是所有操作都需要每个编辑器单独实现。lib/GenericActions.js 提供了一个通用操作scrollList随机滚动列表权重 2所有编辑器 helper 都会合并它。真实输入层面PageDriver.js 封装了click、exists、setInputValue、typeInInput、openContextMenu、scrollList、dragFromTo、describeOverlays、closeAnyMenu、closeAnyOverlay等页面驱动原语——它们通过window.gdVisualTests这个注入到页面中的全局对象工作而closeAnyOverlay会依次尝试点击对话框的 Apply/Ok/Close/Cancel 按钮或按 Escape 键确保每次操作都从干净的界面状态开始。每次操作之后检查什么README 明确列出了这套测试在每一次操作之后的检查清单这也是整个体系的行为契约页面没有抛出任何异常一个未捕获的错误就会打挂整个编辑器编辑器仍然显示着它显示的内容与项目匹配当 helper 能读取项目时操作确实改变了某些东西——因此一个测试不可能在没有实际执行任何操作的情况下通过有精确预期expect的操作得到校验helper 声明的不变式stepChecks成立。这套检查清单在 Runner.js 的runStep/reportStepResult中逐条落地崩溃crashed会打印 THE EDITOR CRASHED及堆栈前几行普通失败打印❌与具体问题每步成功打印✓ 描述 → 效果其中效果由 helper 的describeEffect生成如#0 renamed Player walking to the right、3 → 5 animations。从源码验证一次随机会话的完整调用链把以上各层串起来一条 Storybook monkey 会话的实际调用链是node run.js --suitestorybook --testmonkey→ run.js 加载storybook-tests/下所有测试并过滤名称 → lib/StorybookSuite.js 构建或复用 Storybook、打开对应 story → 在页面中注入window.gdVisualTests并安装 helper 的installPageHelpers→ lib/Runner.js 的runMonkey用种子初始化随机数生成器、按权重抽样操作 → 每个操作经由 lib/PageDriver.js 的真实输入事件执行 →runStep执行操作后五项检查 → 失败/成功写入 lib/Reporter.js 的日志、截图与可选的JUnit 报告。editor 套件则是runEditorSuite先通过 lib/RealEditor.js 获取便携版构建本地 zip 或按分支下载与示例项目以调试端口9333启动应用再把 helper 安装进应用自己打开的窗口而非测试打开的页面然后复用完全相同的runSteps/runMonkey运行器。小结GDevelop 的视觉猴子测试以真实用户 真实浏览器 每次操作后全量校验为设计核心用 Storybook 套件做精确校验、用 editor 套件做真实冒烟再以 seed 可复现的随机会话覆盖脚本化步骤想不到的路径。如果你想为一个新的编辑器接入这套体系helpers/SpriteEditor.js是最完整的范本如果你只关心本地验证node run.js --suitestorybook --headful --testname即可在几秒钟内看到每一步操作与校验结果。这套测试的存在使得改动一行代码就弄挂一个编辑器这类集成级回归在合入主分支之前就会被机器上的真实点击发现。【免费下载链接】GDevelop Open-source, cross-platform 2D/3D/multiplayer game engine designed for everyone.项目地址: https://gitcode.com/GitHub_Trending/gd/GDevelop创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进