- 后端
- 前端
- 开发工具
- 移动开发
【免费下载链接】meteor
Meteor, the JavaScript App Platform
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 run或will 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> | 位置参数 | — | 按用例名称正则过滤 |
--changed | Boolean | false | 只运行自上次通过后发生过变更的文件中的用例 |
--force-online | Boolean | false | 跳过离线检测,强制运行net标签用例 |
--slow | Boolean | false | 包含slow标签的用例 |
--galaxy | Boolean | false | 只运行 galaxy 相关的用例 |
--browserstack | Boolean | false | 使用 Browserstack 客户端 |
--phantom | Boolean | false | 使用 PhantomJS 客户端 |
--headless | Boolean | false | 无头模式,关闭进度条等可视化输出 |
--history <n> | Number | 100 | 失败时打印的最后输出行数 |
--list | Boolean | false | 只列出用例,不执行 |
--file <regexp> | String | — | 按测试文件名正则过滤 |
--exclude <regexp> | String | — | 按用例名称正则排除 |
--with-tag <tag> | String | — | 只运行带指定标签的用例 |
--without-tag <tag> | String | — | 跳过带指定标签的用例 |
--junit <path> | String | — | 输出 JUnit XML 报告到指定路径 |
--retries <n> | Number | 2 | 失败用例的最大重试次数 |
--skip <n> | Number | — | 过滤后跳过前 n 个用例 |
--limit <n> | Number | — | 过滤后最多运行 n 个用例 |
--preview | Boolean | false | 只展示运行计划,不执行 |
离线自动检测
除非显式传入--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); });这段代码完整演示了自测的四个核心步骤:
- 创建 Sandbox:
new Sandbox()建立完全隔离的测试环境(独立目录、独立 HOME、独立会话文件); - 设置环境:
s.set()注入自定义环境变量(此处启用多 mongod 副本集); - 准备应用:
s.createApp()从模板拷贝应用,s.cd()切换工作目录; - 驱动 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/(如empty、failover-test、standard-app、app-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_DIR、METEOR_OFFLINE_CATALOG=t、SANDBOX=true:当使用模拟 warehouse 时设置;METEOR_TEST_LATEST_RELEASE:无模拟 warehouse 时,把当前 release 伪装成最新版;TOOL_NODE_FLAGS:继承SELF_TEST_TOOL_NODE_FLAGS(用于给被测试应用配置 Node 参数)。
此外 run.js 在构造Run时还会注入SELFTEST=t和METEOR_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(命令行参数)、cwd、env、client(浏览器客户端)、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 源码目录下运行 |
galaxy | Galaxy 平台集成测试(--galaxy时启用,且隐含slow+net) |
cordova | 需要 Cordova 支持(Windows 上自动跳过) |
custom-warehouse | 需要自定义模拟 warehouse |
manually-ignored | 通过selftest.skip.define注册,默认排除 |
筛选逻辑在getFilteredTests()(selftest.js)中执行:先按正则、文件、变更状态等附加伪标签(non-matching、in other files、unchanged、excluded、non-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会被设为对应Run的baseTimeout,保证页面加载等待与整体超时机制一致。
高级能力:变更追踪、重试与 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/ | 应用模板(empty、failover-test、standard-app等) |
| tools/tests/packages/ | 包模板(~package-name~占位符替换) |
| tools/tests/fake-mongod/ | mongod 桩进程,配合fakeMongo+tellMongo使用 |
| tools/tool-testing/selftest.js | 框架核心:注册、筛选、断言、报告 |
| tools/tool-testing/sandbox.js | Sandbox 隔离环境实现 |
| tools/tool-testing/run.js | Run 子进程驱动与超时控制 |
| tools/cli/commands.js | self-testCLI 命令注册与全部选项 |
掌握了 Self-Test 的运行命令、Sandbox/Run 双抽象和标签过滤机制后,你就可以为任何 meteor-tool 行为编写端到端用例,并在本地、CI 中按需分片、重试、输出 JUnit 报告,让工具链的质量验证完全自动化。
- 后端
- 前端
- 开发工具
- 移动开发
【免费下载链接】meteor
Meteor, the JavaScript App Platform
相关推荐
Nuxt 测试指南:用 @nuxt/test-utils 编写 Nuxt 运行时单元测试与端到端测试
Nuxt 测试指南:用 @nuxt/test utils 编写 Nuxt 运行时单元测试与端到端测试 本篇基于 Nuxt 官方文档 Testing https:
前端后端Web框架SSRKnative Serving测试框架:如何编写和运行端到端测试
Knative Serving测试框架:如何编写和运行端到端测试 Knative Serving作为基于Kubernetes的Serverless应用平台,其测
云原生后端微服务Milvus 集成测试框架实战指南:基于 MiniClusterV3 的端到端测试编写与运行
Milvus 集成测试框架实战指南:基于 MiniClusterV3 的端到端测试编写与运行 本文基于 Milvus 仓库的 tests/integration
数据库向量数据库分布式数据库后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考