Puppeteer 测试框架 Mocha Runner 源码级解析:TestSuites 与 TestExpectations 双配置驱动的跨浏览器测试运行器
【免费下载链接】puppeteerJavaScript API for Chrome and Firefox项目地址: https://gitcode.com/GitHub_Trending/puppeteer1/puppeteer
导读
Mocha Runner 是 Puppeteer 仓库内部构建于 Mocha 之上的测试运行器:它通过 test/TestSuites.json 与 test/TestExpectations.json 两份 JSON 配置文件,把同一套测试用例以多种浏览器、协议与运行模式(Chrome/Firefox、headless/headful、CDP/WebDriver BiDi)组合驱动执行,并依据预期结果解释测试输出。读完本文,你将掌握它的运行命令、CLI 参数、测试套件定义方式、通配符匹配规则、预期文件字段语义,以及用于调试 flaky 测试(随机失败用例)的内建工具与实现原理。
Mocha Runner 是什么:构建在 Mocha 之上的“配置化”运行器
Mocha Runner(位于 tools/mocha-runner)不是一套独立的断言库或全新的测试框架,而是对 Mocha 的封装层。它做的事情可以概括为两点:
- 按配置批量运行:读取
test/TestSuites.json,把其中的每个测试套件(test suite)翻译成带特定环境变量的 Mocha 子进程,分别执行编译后的test/build/**/*.test.js测试文件; - 按预期解释结果:读取
test/TestExpectations.json,把每个用例的真实结果(通过/失败/超时/跳过)与“预期可接受结果”比对,产出自动化的期望文件修改建议。
项目根目录 package.json 中test任务即执行npx ./tools/mocha-runner,前置依赖为build:tools、./test:build与./tools/mocha-runner:build(经由 wireit 编排),保证运行前所有工具、测试与包均已构建。从工具自身的 package.json 可以看到它名为@puppeteer/mocha-runner,devDependencies中依赖yargs(命令行解析)、zod(JSON Schema 校验)与c8(覆盖率)。
源码布局
- tools/mocha-runner/src/mocha-runner.ts:CLI 入口与主流程,负责解析参数、装配环境、spawn Mocha 子进程并比对结果;
- tools/mocha-runner/src/types.ts:zod schema 与
TestExpectation、MochaResults等核心类型定义; - tools/mocha-runner/src/utils.ts:平台/参数过滤、模式匹配、预期更新(add/remove/update)等纯函数;
- tools/mocha-runner/src/interface.cts:自定义 Mocha BDD 接口(保持 CJS,因 Mocha 自定义接口尚不直接接受 ESM),实现 SKIP 注入与 deflake 逻辑;
- tools/mocha-runner/src/reporter.cts:同时继承
Mocha.reporters.Spec与JSON的自定义 reporter,把结构化结果写入文件供上层分析; - tools/mocha-runner/src/test.ts:针对
utils.ts各函数的单元测试。
运行 Mocha Runner:自测、全量跑与指定套件
运行 Mocha Runner 自身的单元测试
npm test在tools/mocha-runner目录下执行该命令会编译并运行test.ts(wireit 中对应c8 node ./bin/test.js),验证过滤、匹配与预期更新逻辑的正确性。
使用 Mocha Runner 运行 Puppeteer 测试
npm run build && npm run test在仓库根目录执行。默认情况下,运行器会执行所有适用于当前平台的测试套件——平台由os.platform()解析(win32/linux/darwin,见 types.ts 中的zPlatform)。
如果只想跑某一个套件,用--test-suite指定套件 id。例如仅运行 Chrome headless 模式的 CDP 套件:
npm run build && npm run test -- --test-suite chrome-headless套件 id 必须定义在test/TestSuites.json中;若未找到,运行器会打印Test suite <id> is not defined并以退出码 1 结束(见 mocha-runner.ts 的getApplicableTestSuites)。
更多 CLI 选项
根 package.json 已按套件封装了便捷脚本(如test:chrome:headless、test:chrome:bidi、test:firefox:headless等),而运行器本身在 mocha-runner.ts 中还暴露了以下参数:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
--test-suite <id> | string | 全部套件 | 只运行指定 id 的测试套件 |
--suggestions | boolean | true | 结果与预期不符时打印期望文件修改建议 |
--cdp-tests | boolean | true | 是否包含test/build/cdp/目录下的 CDP 专属测试 |
--save-stats-to <path> | string | 临时目录 | 保存 Mocha JSON 结果的文件路径,支持用INSERTID占位符替换为随机 UUID |
--min-tests <n> | number | 0 | 非分片运行时发现用例总数低于该值即判定失败(用于捕获“跑空”问题) |
--shard <id-total> | string | 无 | 分片执行,如1-4表示 4 个分片中的第 1 片 |
--reporter <path> | string | 内置 reporter | 自定义 Mocha reporter |
--print-memory | boolean | false | 追加--expose-gc等 Node 参数以观测内存 |
--ignore-unexpectedly-passing | boolean | false | 忽略“预期 FAIL/SKIP 但实际 PASS”的用例,不生成修改建议 |
--coverage | boolean | false | 用c8 --check-coverage --lines 90包裹 Mocha 进程做覆盖率检查 |
配置了parserConfiguration({'unknown-options-as-args': true})(mocha-runner.ts),因此其余未识别参数(如 Mocha 自身的--grep)会被原样透传给 Mocha 进程。
TestSuites.json:用 parameterDefinitions 声明组合矩阵
运行器把每个测试套件定义为一个{id, parameters[]}对象;parameters中的每个词都对应parameterDefinitions里的一组环境变量定义。测试执行时这些环境变量会被合并进 Mocha 子进程,从而改变 Puppeteer 被实例化时的浏览器与协议选择。含义上:parameters 决定“用哪套环境跑测试”,也作为 TestExpectations.json 中按参数禁用/标注用例的匹配键。
当前仓库的 TestSuites.json 定义了 8 个套件:
| id | parameters | 组合含义 |
|---|---|---|
chrome-headless | chrome、headless、cdp | Chrome 无头 + CDP |
chrome-headful | chrome、headful、cdp | Chrome 有头 + CDP |
chrome-headless-shell | chrome、chrome-headless-shell、cdp | Chrome Headless Shell + CDP |
firefox-headless | firefox、headless、webDriverBiDi | Firefox 无头 + BiDi |
firefox-headful | firefox、headful、webDriverBiDi | Firefox 有头 + BiDi |
chrome-bidi | chrome、headless、webDriverBiDi | Chrome 无头 + WebDriver BiDi |
chrome-bidi-only | chrome、headless、webDriverBiDiOnly | Chrome 无头 + 仅 BiDi 协议 |
chrome-pipe | chrome、headless、cdp、pipe | Chrome 无头 + CDP over pipe |
对应parameterDefinitions将参数翻译为真实环境变量,例如:
chrome→PUPPETEER_BROWSER=chrome,firefox→PUPPETEER_BROWSER=firefox;headless→HEADLESS=true,headful→HEADLESS=false,chrome-headless-shell→HEADLESS=shell;webDriverBiDi→PUPPETEER_PROTOCOL=webDriverBiDi,webDriverBiDiOnly额外追加PUPPETEER_WEBDRIVER_BIDI_ONLY=true;cdp无额外变量,pipe→PUPPETEER_PIPE=true。
这些环境变量会被传给真正启动 Puppeteer 的测试代码,因此同一份.test.ts无需任何改动即可在多浏览器/多协议矩阵下运行。注意该文件的结构在 types.ts 中由 zod 校验:testSuites中每个套件必须含字符串id与字符串数组parameters,而parameterDefinitions被解析为Record<string, unknown>。
TestExpectations.json:预期(Expectation)字段语义
一份典型的预期记录形如:
{ "testIdPattern": "[accessibility.spec]", "platforms": ["darwin", "win32", "linux"], "parameters": ["firefox"], "expectations": ["SKIP"] }字段含义与匹配逻辑汇总如下(文档 tools/mocha-runner/README.md 定义,匹配逻辑可由 utils.ts 佐证):
| 字段 | 说明 | 类型 | 匹配逻辑 |
|---|---|---|---|
testIdPattern | 用于匹配用例全名(或其模式)的字符串 | string | — |
platforms | 该预期适用的操作系统平台 | Array<linux|win32|darwin> | OR |
parameters | 测试需具备的参数(取自parameterDefinitions) | Array | AND |
expectations | 视为可接受的测试结果列表 | Array<PASS|FAIL|TIMEOUT|SKIP> | OR |
真实仓库文件 test/TestExpectations.json 还普遍带有comment字段(非强制),用于记录原因,例如"[bfcache.test] *"因 chromium-bidi issue 而 SKIP、"[bluetooth-emulation.test] *"因 Headless Shell 不支持蓝牙模拟而期望 FAIL。
platforms的 OR 语义体现在filterByPlatform:只要item.platforms.includes(platform)即为命中;parameters的 AND 语义体现在filterByParameters:只有当用例套件的参数集合包含预期里列出的全部参数时才命中。源码 utils.ts 用querySet.has(param)对ex.parameters.every(...)做判断,因此预期parameters: ["firefox","headless"]同时匹配套件firefox-headless与firefox-headful的前者(后者不满足 AND,不命中);仅写["firefox"]则会同时影响 headless 与 headful 两个套件。
两条关键的优先级规则
文档强调了两条重要规则,它们与源码中的处理顺序一一对应:
- 预期定义的顺序很重要:后定义的预期优先于先定义的。
- 只要
expectations中含SKIP,无论是否还有其他预期,该测试都会被跳过。
第一点可在 mocha-runner.ts 得到印证:运行器先按平台、再按参数过滤出适用预期,随后调用.reverse()反转数组,findEffectiveExpectationForTest用find取第一个命中的记录(utils.ts),因此文件中越靠后的记录优先级越高。第二点由 interface.cts 中的shouldSkipTest保证:跳过表PUPPETEER_SKIPPED_TEST_CONFIG会把每条预期的skip(即expectations.includes('SKIP'))与testIdPattern一并下发,接口层命中即不注册真实测试体,而以空测试占位,从而避免用例真正运行。
testIdPattern 通配符:用*圈定一批用例
Puppeteer 测试全名的规范格式为[文件名] 父级describe… 用例title(如[jshandle.spec] JSHandle JSHandle.toString should work for primitives)。testIdPattern中的*使用贪婪匹配(greedy method),用于一次性圈定整组用例。文档给出了三种典型用法:
| 模式 | 说明 | 示例模式 | 示例命中 |
|---|---|---|---|
* | 匹配所有测试 | — | — |
[test.spec] * | 匹配该文件内的所有测试 | [jshandle.spec] * | [jshandle] JSHandle JSHandle.toString should work for primitives |
[test.spec] <text> * | 匹配带指定前缀(通常是 describe 节点)的测试 | [page.spec] Page Page.goto * | [page.spec] Page Page.goto should work、[page.spec] Page Page.goto should work with anchor navigation |
[test.spec] * <text> | 匹配带指定后缀的测试 | [navigation.spec] * should work | [navigation.spec] navigation Page.goto should work、[navigation.spec] navigation Page.waitForNavigation should work |
其底层实现位于 utils.ts 的testIdMatchesExpectationPattern:先把*替换为占位符--STAR--,再转义所有正则特殊字符,最后把占位符还原为(.*)?(贪婪),并显式锚定^...$,随后用[文件基名] 完整title拼接出的 testId 做整串匹配。[test.spec] Page *这类模式只会命中“Page 前缀下的用例”,不会误伤Page2下的用例——因为中间有显式的空格字面量。该函数行为在 test.ts 中拥有覆盖 15 组正反例的单元测试。
期望文件如何更新:运行失败时自动给出 add/remove/update 建议
文档指出:目前 expectations 是手工维护的,但运行器会在测试结果与预期不一致时输出建议的修改。这是本文档所述工作流中最实用的自动化能力之一。
其比对逻辑在getExpectationUpdates(utils.ts),按三类动作生成建议,最终在进程收尾阶段由 mocha-runner.ts 分类打印:
- remove:某用例被预期为
FAIL却实际 PASS,且命中的预期不含通配符 → 建议删除该条预期; - add:某个用例 FAIL/TIMEOUT 但没有任何预期覆盖 → 建议新增一条精确到用例的预期;若命中的是一条通配符预期,为避免误伤同组其他用例,也会建议新增一条精确的独立预期,并把原通配符记录在
basedOn; - update:精确预期存在但缺失实际出现的结果类型(如预期
FAIL却超时)→ 建议把TIMEOUT追加进expectations。
失败结果类型由 utils.ts 判定:错误码为ERR_MOCHA_TIMEOUT记TIMEOUT,否则记FAIL。结果中还附带了参数、平台与时间戳,若更新数与失败标记同时存在,进程以退出码 1 结束,保证 CI 在“结果偏离预期”时能立即感知。若开启--ignore-unexpectedly-passing,PASS 分支将被跳过(见 test.ts 中对skipPassing=true的用例测试)。
调试 Flaky 测试:日志捕获与自动重跑机制
对随机失败用例,Mocha Runner 提供了两层手段:代码内的工具函数,与无需改测试的环境变量开关。
内建 Utility 函数
在测试文件中可直接使用 Mocha Runner 扩展的 BDD 接口(由 interface.cts 通过module.exports = customBDDInterface注册为Custom BDD接口,其description为'Custom BDD'):
| Utility | 参数 | 作用 |
|---|---|---|
describe.withDebugLogs | (title, <DescribeBody>) | 为包裹的每个用例开启日志捕获,并在失败时打印捕获到的 debug 日志 |
it.deflake | (repeat, title, <itFunction>) | 将用例重跑 N 次,失败的那几次会附带打印 debug 日志 |
it.deflakeOnly | (repeat, title, <itFunction>) | 同it.deflake,但只运行这一个用例(等价it.only版) |
实现细节:describe.withDebugLogs在beforeEach中调用puppeteer-core/internal/common/Debug.js暴露的setLogCapture(true),在afterEach中通过dumpLogsIfFail依据this.currentTest?.state === 'failed'判断并把getCapturedLogs()打印出来;it.deflake/it.deflakeOnly则由wrapDeflake将用例复制为1/title、2/title… 并置于withDebugLogs容器内循环重跑。
环境变量开关:零侵入式 deflake
若不想改动任何测试代码,可以直接设置环境变量,运行器会自动把命中的用例包裹进describe.withDebugLogs。文档示例:
PUPPETEER_DEFLAKE_TESTS="[navigation.spec] navigation Page.goto should navigate to empty page with networkidle0" npm run test:chrome:headless与TestExpectations.json一样,这里同样支持*通配符模式(底层复用testIdMatchesExpectationPattern):
PUPPETEER_DEFLAKE_TESTS="[navigation.spec] *" npm run test:chrome:headless重跑次数默认为100 次,可通过PUPPETEER_DEFLAKE_RETRIES调整(interface.cts 将其默认值设为100):
PUPPETEER_DEFLAKE_RETRIES=1000 PUPPETEER_DEFLAKE_TESTS="[navigation.spec] *" npm run test:chrome:headless命中 deflake 的用例会被放进带日志捕获的独立 Suite,通过test.clone()复制出deflakeRetries份逐一执行(interface.cts),从而在不修改测试的前提下大规模重放可疑用例并采集失败现场日志。
主流程串讲:一次典型运行发生了什么
综合 mocha-runner.ts 的实现,一次典型运行可归纳为以下步骤:
- 解析 CLI 参数,确定
--test-suite(缺省则取TestSuites.json中全部套件); - 读取并校验 test/TestExpectations.json 与 test/TestSuites.json,由
zPlatform/zTestSuiteFile做类型校验; - 对每个适用套件:把
parameters逐项查表展开成环境变量,按平台/参数过滤预期并反转,进而构造PUPPETEER_SKIPPED_TEST_CONFIG(JSON 化的 testIdPattern→skip 映射)注入子进程环境; - 在 CI 环境(
RUNNER_DEBUG开启,即 GitHub Actions Debug 日志开关)下追加NODE_DEBUG=puppeteer:*与EXTRA_LAUNCH_OPTIONS(dumpio: true、Firefoxremote.log.level=Trace)以便排查启动问题;同时在打印环境变量时只筛出PUPPETEER_前缀项以免泄露敏感信息(utils.ts); - 收集
test/build/**/*.test.js(默认含 cdp 目录,可用--cdp-tests=false排除),按--shard分片或全量传入,spawnmocha/bin/mocha.js子进程,加载自定义 interface/reporter,并把结构化 JSON 写入--save-stats-to指定文件或临时目录; - 读取结果 JSON(含
stats/pending/passes/failures),计算getExpectationUpdates,输出 add/remove/update 建议; - 存在偏差、分片为空或用例数低于
--min-tests时退出码为 1,否则退出码 0 并打印Run succeeded.。
值得一提的健壮性处理:若 Mocha 进程被信号终止或未产出结果文件,运行器会抛出包含退出码/信号原因的明确错误(mocha-runner.ts);若失败发生在 hook 阶段(无 file 关联的错误),也会单独记录并建议新增预期。
小结
Mocha Runner 的核心理念是把“测试用例”与“测试环境组合、可接受结果”彻底解耦:用例只需按 BDD 风格编写一次,浏览器矩阵(Chrome/Firefox × headless/headful × CDP/BiDi/pipe)完全由 TestSuites.json 的参数化声明驱动,跨平台差异与已知缺陷则沉淀为 TestExpectations.json 中带通配符与注释的预期记录;再加上自动 diff 建议与 flaky 用例 deflake 重放机制,使得这套“单仓库、多浏览器、持续集成”的测试体系可以长期低成本地维护。若你需要在本仓库扩展新的浏览器组合,只需在TestSuites.json中新增一条带独立参数的 suite,并在parameterDefinitions中补充对应环境变量即可。
【免费下载链接】puppeteerJavaScript API for Chrome and Firefox项目地址: https://gitcode.com/GitHub_Trending/puppeteer1/puppeteer
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考