Meteor 自测框架(Self-Test)完全指南:端到端测试的运行、编写与内部原理
2026/9/20 9:21:16 网站建设 项目流程
  • 后端
  • 前端
  • 开发工具
  • 移动开发

【免费下载链接】meteor

Meteor, the JavaScript App Platform

项目地址:https://gitcode.com/gh_mirrors/me/meteor
点击查看免费下载

Meteor 的 Self-Test(自测)是运行在真实meteor命令之上的端到端测试体系,它通过./meteor self-test启动,用一套独立的Sandbox/Run抽象在隔离环境中创建应用、执行 CLI 命令并断言输出。本文以 tools/tool-testing/README.md 为主线,结合 selftest.js、sandbox.js、run.js 等实现源码,完整讲解如何运行、筛选、编写自测用例,并剖析其背后的超时控制、标签过滤、测试状态记录等机制,帮助你为 Meteor 工具链写出可维护的端到端测试。

什么是 Self-Test

Self-Test 是 Meteor 工具(meteor-tool)自身的端到端测试框架,它不 mock 内部模块,而是直接以子进程方式启动真实的meteor可执行文件,模拟用户在终端中的完整操作流:创建应用、修改文件、运行命令、观察输出、验证退出码。所有测试用例存放在 tools/tests/ 目录下,每个.js文件通过调用selftest.define()注册若干用例。

运行 Self-Test 有一个硬性前提:只能在 checkout 源码目录下运行。在 tools/cli/commands.js 中可以看到明确校验:

if (! files.inCheckout()) { Console.error("self-test is only supported running from a checkout"); return 1; }

因此你需要先通过 git 拉取 Meteor 源码仓库,再在仓库根目录执行自测命令。

快速开始:运行 Self-Test

在仓库根目录执行:

./meteor self-test <regexp>

<regexp>是一个可选的 JavaScript 正则表达式,用于按用例名称过滤测试。例如只运行名称中包含mongo的用例:

./meteor self-test mongo

若不传正则,则运行全部可用用例。命令本身在 tools/cli/commands.js 中注册(hidden: true,属隐藏命令),最多接受 1 个位置参数。

慢机器上的时间缩放:TIMEOUT_SCALE_FACTOR

自测中大量操作(等待应用启动、等待输出匹配)都有超时限制。在慢速机器上,可以通过环境变量把所有超时统一放大

# Unix / macOS(bash/zsh) export TIMEOUT_SCALE_FACTOR=3 # Windows(cmd) set TIMEOUT_SCALE_FACTOR=3

其底层实现在 tools/utils/utils.js:

var timeoutScaleFactor = 1.0; if (process.env.TIMEOUT_SCALE_FACTOR) { timeoutScaleFactor = parseFloat(process.env.TIMEOUT_SCALE_FACTOR); }

而 run.js 中每次匹配的超时计算为:

let timeout = this.baseTimeout + this.extraTime; // baseTimeout 默认 20 秒 timeout *= timeoutScaleFactor; // 乘以缩放因子

即默认基础超时为 20 秒,waitSecs(n)累加的额外时间与之相加后,整体乘以TIMEOUT_SCALE_FACTOR。设置为3意味着所有超时放大三倍,适合 CI 或性能较弱的开发机。

预览:--preview

执行前先用--preview查看筛选后哪些用例会运行、哪些会被跳过,而不真正执行

./meteor self-test --preview

从 selftest.js 的实现看,预览模式会对每个用例打印will runwill skip,随后直接进入下一个用例。

分批执行:--skip 与 --limit

--skip <number>--limit <number>用于把测试分批跑完。它们的语义是在正则过滤之后再做跳过/限制,而不是二次过滤:

# 第一批:前 50 个 ./meteor self-test --limit 50 # 第二批:跳过前 50 个,再取 50 个 ./meteor self-test --skip 50 --limit 50

对应逻辑在 selftest.js 的shouldSkipCurrentTest

if (limit && skip) { return currentTestIndex < skip || (currentTestIndex - skip) >= limit; } if (limit) { return currentTestIndex >= limit; } if (skip) { return currentTestIndex < skip; }

注意:--skip/--limit作用于已经过正则、标签等过滤后的最终列表,所以分批时各批之间的用例集合不会因为过滤规则而错位,适合在本地分片跑大批量用例。

完整命令行选项

self-test命令在 tools/cli/commands.js 中注册了以下选项:

选项类型默认值作用
<regexp>位置参数按用例名称正则过滤
--changedBooleanfalse只运行自上次通过后发生过变更的文件中的用例
--force-onlineBooleanfalse跳过离线检测,强制运行net标签用例
--slowBooleanfalse包含slow标签的用例
--galaxyBooleanfalse只运行 galaxy 相关的用例
--browserstackBooleanfalse使用 Browserstack 客户端
--phantomBooleanfalse使用 PhantomJS 客户端
--headlessBooleanfalse无头模式,关闭进度条等可视化输出
--history <n>Number100失败时打印的最后输出行数
--listBooleanfalse只列出用例,不执行
--file <regexp>String按测试文件名正则过滤
--exclude <regexp>String按用例名称正则排除
--with-tag <tag>String只运行带指定标签的用例
--without-tag <tag>String跳过带指定标签的用例
--junit <path>String输出 JUnit XML 报告到指定路径
--retries <n>Number2失败用例的最大重试次数
--skip <n>Number过滤后跳过前 n 个用例
--limit <n>Number过滤后最多运行 n 个用例
--previewBooleanfalse只展示运行计划,不执行

离线自动检测

除非显式传入--force-online,框架启动时会尝试请求http://www.google.com/探测网络;若判定离线,则自动跳过带net标签的用例(见 tools/cli/commands.js)。因此在没有外网的环境(如飞机上、内网 CI)中,默认行为是自动排除需要联网的用例。

编写第一个自测用例

注册机制

所有测试存放在 tools/tests/ 目录。框架启动时(selftest.js)会遍历该目录下所有以.js结尾的文件,require它们,文件内的selftest.define()调用即完成注册:

export function define(name, tagsList, f) { if (typeof tagsList === "function") { // tagsList 是可选参数,可以省略 f = tagsList; tagsList = []; } const tags = tagsList.slice(); tags.sort(); allTests.push(new Test({ name, tags, file: fileBeingLoaded, fileHash: fileBeingLoadedHash, func: f, })); }

每个用例包含:name(用例名)、tags(排序后的标签数组)、file(所在文件名)、fileHash(文件内容的 SHA-1,用于--changed变更追踪)、func(测试函数体)。

另外,selftest.js 还提供了selftest.skip.define(name, tags, f),用于注册一个默认被跳过的用例(会被自动打上manually-ignored标签),适合暂时禁用某些失败用例。

内置断言

自测框架在 selftest.js 中提供了一组轻量断言:

API作用
fail(reason)直接抛出TestFailure使用例失败
expectEqual(actual, expected)使用 EJSON 深度比较两个值是否相等
expectTrue(actual)断言值为真值
expectFalse(actual)断言值为假值
expectThrows(f)断言调用f()会抛出异常

这些断言都会通过parseStackMarkTop修饰,保证失败时打印的调用栈从测试代码开始,而不是从断言实现内部开始。

完整示例:MongoDB failover 测试

README 中给出的典型示例,其现代版本实现在 tools/tests/mongo.js:

var selftest = require('../tool-testing/selftest.js'); var Sandbox = selftest.Sandbox; // 验证 observeChanges 在 MongoDB failover 后仍然正常工作 selftest.define("mongo failover", ["slow"], async function () { var s = new Sandbox(); await s.init(); s.set('METEOR_TEST_MULTIPLE_MONGOD_REPLSET', 't'); await s.createApp("failover-test", "failover-test"); s.cd("failover-test"); var run = s.run("--once", "--raw-logs"); run.waitSecs(120); await run.match("SUCCESS\n"); await run.expectEnd(); await run.expectExit(0); });

这段代码完整演示了自测的四个核心步骤:

  1. 创建 Sandboxnew Sandbox()建立完全隔离的测试环境(独立目录、独立 HOME、独立会话文件);
  2. 设置环境s.set()注入自定义环境变量(此处启用多 mongod 副本集);
  3. 准备应用s.createApp()从模板拷贝应用,s.cd()切换工作目录;
  4. 驱动 meteor 命令s.run("--once", "--raw-logs")启动真实 meteor 进程,run.match()匹配 stdout、run.expectEnd()断言输出结束、run.expectExit(0)断言退出码为 0。

waitSecs(120)为下一步操作额外增加 120 秒超时(MongoDB failover 场景耗时较长)。注意示例中的await:框架支持异步测试函数,s.init()等初始化必须await

测试模板

应用模板位于 tools/tests/apps/(如emptyfailover-teststandard-appapp-config等),包模板位于 tools/tests/packages/。createApp(to, template)会从模板目录cp_r拷贝整个应用(自动忽略local目录),并确保应用不触发额外 upgrader;模板中的~package-name~占位符会被createPackage()替换为真实包名(见 sandbox.js)。

Sandbox:隔离的测试环境

Sandbox是自测的核心抽象,代表一个独立的 meteor 工具安装。创建 Sandbox 会生成一个临时目录(files.mkdtemp()),内部再建home目录作为该沙箱的 HOME,所有运行都指向沙箱内的 meteor 脚本,与用户真实环境、其他沙箱互不干扰(sandbox.js)。

注意:new Sandbox()若需要构建包来准备环境,构建失败时会抛出TestFailure,因此只能在测试函数内部调用。

Sandbox 常用方法

方法说明
run(...args)启动一次 meteor 运行,返回Run对象
createApp(to, template, options?)从 tools/tests/apps 模板拷贝应用到沙箱
createPackage(dir, name, template)从 tools/tests/packages 模板创建包
cd(relPath, callback?)切换后续运行的 cwd;传回调时执行后自动还原
set(name, value)/unset(name)设置/清除后续运行的环境变量
write(filename, contents)写文件(相对于沙箱 cwd,utf8)
append(filename, contents)追加文件
read(filename)/readDir(dir)读文件/目录(不存在返回null
cp(from, to)在沙箱内复制文件(常用于切换 package.js 备份内容)
mkdir/unlink/rename目录与文件操作
readSessionFile()/writeSessionFile(contents)读取/覆盖.meteorsession,可保存与恢复登录态

沙箱自动注入的环境变量

每次run()都会通过_makeEnv()(sandbox.js)注入关键环境变量:

  • METEOR_SESSION_FILE:指向沙箱内独立的.meteorsession,登录态不污染真实用户;
  • METEOR_WAREHOUSE_DIRMETEOR_OFFLINE_CATALOG=tSANDBOX=true:当使用模拟 warehouse 时设置;
  • METEOR_TEST_LATEST_RELEASE:无模拟 warehouse 时,把当前 release 伪装成最新版;
  • TOOL_NODE_FLAGS:继承SELF_TEST_TOOL_NODE_FLAGS(用于给被测试应用配置 Node 参数)。

此外 run.js 在构造Run时还会注入SELFTEST=tMETEOR_NO_WORDWRAP=t,标识自测进程并关闭输出折行。

模拟 warehouse 与 fakeMongo

  • new Sandbox({ warehouse: {...} })可构造模拟发布仓库:传入形如{ version1: { tool: 'tools1' }, version2: { recommended: true, tool: 'tools2', upgraders: ['a'] } }的配置,框架会从 checkout 构建出对应的模拟 release 集合(仅支持 checkout 模式),用于测试版本切换、upgrader 等逻辑(sandbox.js)。
  • new Sandbox({ fakeMongo: true })会让应用启动 tools/tests/fake-mongod 中的 mongod 桩进程,之后可通过Run.tellMongo()向桩发送控制命令(详见下文),用于模拟 MongoDB 崩溃、failover 等故障场景。

Run:驱动 meteor 子进程

Run对象代表一次对 meteor 可执行文件的测试运行,通过Sandbox.run(...args)创建。其构造函数(run.js)支持args(命令行参数)、cwdenvclient(浏览器客户端)、fakeMongo等选项,并默认设置 20 秒基础超时。

输出匹配:match 系列

match(pattern)等待 stdout 中出现匹配文本(正则或字符串),并消费到该位置为止的输出;超时或进程先退出而未匹配则用例失败。相关方法:

方法说明
match(pattern)在 stdout 中向前搜索匹配
matchErr(pattern)在 stderr 中向前搜索匹配
read(pattern)类似 match,但不允许跳过——必须紧跟上次匹配/读取的位置
readErr(pattern)read 的 stderr 版本
matchBeforeExit(pattern)匹配一次性模式,不敏感于顺序(进程结束前完成匹配即可)
matchErrBeforeExit(pattern)同上,针对 stderr
getMatcherFullBuffer()获取到目前为止的完整输出缓冲

匹配由 matcher.js 中的Matcher异步实现:输出以流方式写入缓冲区并尝试匹配,超时产生match-timeout失败,进程退出仍有未消费输出则产生junk-before失败。

退出断言:expectExit / expectEnd

await run.expectExit(0); // 断言进程以退出码 0 结束(可省略参数,仅等待退出) await run.expectEnd(); // 断言退出且 stdout/stderr 均无多余输出

expectEnd()expectExit()基础上,进一步要求两个输出流都被完全消费干净,防止测试漏掉意外输出。若实际退出码与期望不符,会抛出wrong-exit-code失败并同时打印期望值与实际值(含 signal)。

禁止模式:forbid 系列

run.forbid(regexp); // 整个运行过程中 stdout 不得出现该模式 run.forbidErr(regexp); // stderr 不得出现 run.forbidAll(regexp); // 两个流都不得出现

⚠️重要陷阱(README 明确提示):forbid()检查的是从运行开始到当前的全部输出,而不是从调用点之后的输出——因为输出是异步流式匹配的。所以forbid应当在进程结束后(如expectExit之后)调用,才能覆盖完整输出。

其他控制方法

  • waitSecs(secs):为下一次操作(match/expectExit 等)增加额外超时秒数;
  • write(string):向进程 stdin 写入内容(模拟交互输入);
  • stop():杀掉进程并等待退出;内部_killProcess()在 Windows 上使用taskkill /f /t以连子进程一起终止;
  • connectClient():若 Run 带客户端创建,连接浏览器客户端(用于客户端测试)。

失败原因一览

run.js 的runTest会对失败进行分类打印,常见TestFailure原因包括:

失败原因含义
spawn-failure进程未能成功启动
exit-timeout进程超时未退出
wrong-exit-code退出码与期望不符
match-timeout匹配超时
no-match进程结束时仍未匹配到模式
junk-before匹配后存在未消费的输出
not-equal/not-true/not-false断言失败
expected-exception期望抛异常但未抛出
mongo-not-running无法连接到 fake-mongod 桩

失败时默认打印最后 100 行输出(可用--history <n>调整),并自动过滤掉tools/tool-testing内部栈帧,直接定位到tools/tests/xxx.js:行号

Tags 标签:分类、过滤与跳过

标签是自测的元数据机制,注册时传入的任意字符串数组都会被排序后存入用例。标签本身只是数据,要让标签产生过滤效果,需要修改 selftest.js 中的筛选逻辑——README 对此有明确说明。

框架内置了以下标签语义(见 selftest.js 的tagDescriptions与注册注释):

标签语义
slow测试很耗时,只有传--slow才运行
windows仅在 Windows 上运行
net需要访问外部网络服务,离线时自动跳过
checkout只能在 checkout 源码目录下运行
galaxyGalaxy 平台集成测试(--galaxy时启用,且隐含slow+net
cordova需要 Cordova 支持(Windows 上自动跳过)
custom-warehouse需要自定义模拟 warehouse
manually-ignored通过selftest.skip.define注册,默认排除

筛选逻辑在getFilteredTests()(selftest.js)中执行:先按正则、文件、变更状态等附加伪标签(non-matchingin other filesunchangedexcludednon-galaxy),再按平台自动跳过windows(非 Windows)或cordova/yet-unsolved-windows-failure(Windows)。命令行可通过--with-tag/--without-tag进一步精确控制。

浏览器客户端测试:testWithAllClients

对于需要真实浏览器环境的应用,Sandbox 提供testWithAllClients(f, options)方法:为每个已启用的客户端各创建一个Run,并执行回调f(run)(sandbox.js):

await s.testWithAllClients(async function (run) { run.connectClient(); // 连接浏览器客户端 await run.match("some text"); // 匹配应用页面输出 }, { testName: "my client test", testFile: "client-tests", args: ["--once"], });

客户端默认指向应用的localhost:3000(可通过clients.port修改)。支持的客户端类型:

  • Puppeteer:默认总是启用(见 tools/cli/commands.js);
  • Phantom:传--phantom启用;
  • BrowserStack:传--browserstack启用,且需满足其前置条件(SDK 凭据等),不满足时自动忽略。

客户端的timeout会被设为对应RunbaseTimeout,保证页面加载等待与整体超时机制一致。

高级能力:变更追踪、重试与 JUnit 报告

--changed:只跑变更过的用例

框架会把每个测试文件的 SHA-1 哈希与通过状态记录在~/.meteortest(版本 1 的 JSON,字段lastPassedHashes)。传--changed时,若某文件当前哈希与上次全部通过时的哈希一致,则该文件用例被标记为unchanged并跳过(selftest.js)。这非常适合日常开发:先全量跑一遍建立基线,之后只验证改动过的文件。

--retries:失败自动重试

--retries <n>默认值为2。单个用例失败时,runTest会在重试次数耗尽前重新执行整个用例函数(run.js),对偶发性的时序问题(如端口竞争)有很好的容错。注意每次重试都会重新执行用例完整流程,因此用例本身必须可重复执行。

--junit:CI 集成

--junit <path>会生成标准的 JUnit XML 报告:每个测试文件对应一个<testsuite>,每个用例对应一个<testcase>,失败时嵌入<error>/<failure>元素及失败详情(selftest.js),可直接接入 Jenkins、GitLab CI 等平台的测试报告解析。

常见陷阱(Gotchas)

  • Self-Test 的文档就是代码本身:README 明确指出 "The docs for self-test is reading the code of self-test"——框架没有独立的规范文档,所有行为细节以 selftest.js 源码为准,阅读源码是最可靠的参考。
  • forbid()是全局匹配:它约束整个运行的输出,而非调用点之后的输出(异步流式匹配所致)。应在expectExit/expectEnd之后调用。
  • 必须在 checkout 中运行./meteor self-test会直接拒绝在已发布版本环境执行。
  • Sandbox 只能在测试函数内创建:准备环境需要构建包,失败会抛TestFailure,不要在测试外复用。
  • read()不跳读:与match()不同,read()严格要求模式紧跟上次消费位置,否则会得到junk-before失败。
  • 标签需要改代码才生效:新增标签语义必须修改 selftest.js 的筛选逻辑。

测试资产速览

路径内容
tools/tests/全部自测用例文件(每个.js文件用selftest.define注册)
tools/tests/apps/应用模板(emptyfailover-teststandard-app等)
tools/tests/packages/包模板(~package-name~占位符替换)
tools/tests/fake-mongod/mongod 桩进程,配合fakeMongo+tellMongo使用
tools/tool-testing/selftest.js框架核心:注册、筛选、断言、报告
tools/tool-testing/sandbox.jsSandbox 隔离环境实现
tools/tool-testing/run.jsRun 子进程驱动与超时控制
tools/cli/commands.jsself-testCLI 命令注册与全部选项

掌握了 Self-Test 的运行命令、Sandbox/Run 双抽象和标签过滤机制后,你就可以为任何 meteor-tool 行为编写端到端用例,并在本地、CI 中按需分片、重试、输出 JUnit 报告,让工具链的质量验证完全自动化。

  • 后端
  • 前端
  • 开发工具
  • 移动开发

【免费下载链接】meteor

Meteor, the JavaScript App Platform

项目地址:https://gitcode.com/gh_mirrors/me/meteor
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询