MongoDB 仓库 JavaScript 集成测试代码规范与 mochalite 实践指南
【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo
导读
MongoDB 服务器仓库中的jstests/目录承载了数千个 JavaScript 集成测试,这些测试通过 mongo shell 驱动真实运行的 mongod / mongos(单机、副本集或分片集群)来验证服务器行为。本文基于仓库 jstests/AGENTS.md(由 jstests/CLAUDE.md 引用的 JavaScript 测试代码规范)展开,并结合 jstests/libs/mochalite.js、src/mongo/shell/assert.js 等核心源码,系统讲解断言库的正确用法、mocha 风格的测试组织方式、结构化日志的输出规范以及资源清理要求。读完本文,你将掌握在 MongoDB 源码仓库中编写符合社区标准、可读可调试、确定性强的 JavaScript 集成测试的完整方法。
一、规范总览:从 CLAUDE.md 到 AGENTS.md
jstests/CLAUDE.md本身只包含一行引用@AGENTS.md,它指向同目录下真正的规范文档 jstests/AGENTS.md。这份规范共四大主题,是 MongoDB 服务器 JavaScript 测试开发的“军规”:
- 断言库(Assertion Library):必须使用项目自有的断言库,并区分同步命令断言与异步条件断言。
- 新测试文件采用 mocha 风格:使用
describe/it/ 各类钩子组织用例。 - 日志中使用 JSON 兼容的对象序列化:通过
attr参数传递结构化数据,而不是把tojson()拼进字符串。 - 资源与设置清理:用完后立即释放游标、集合等资源,并复位服务器参数与
TestData全局配置。
同时,jstests/README.md 这份更完整的《Javascript Test Guide》从“最小化测试用例、可调试性、确定性、尽早失败、测试隔离”等原则对上述规范做了系统展开,可作为补充阅读。
二、断言库:统一使用项目自有断言
规范第一条是ALWAYS 使用项目自有的断言库。该库定义在 src/mongo/shell/assert.js(共 2200+ 行),在每次测试运行前被自动加载进全局作用域,因此测试文件里无需 import 即可直接使用assert。
2.1 通用断言与错误抛出的底层机制
通用断言assert(b, msg, attr)接收布尔值b,为假时抛出错误。其底层由doassert()完成:msg可以是字符串、函数(运行时求值)或对象(自动tojson序列化),若传入attr,则通过_getErrorWithCode构造带结构化属性的错误对象,见 assert.js。失败时若TestData.logFormat === "json",会以 JSON 形式输出结构化日志,错误对象上会附带extra字段保存attr内容。
2.2 命令断言:commandWorked / commandFailedWithCode
规范明确要求:除非预期失败并显式检查,否则 ALWAYS 用assert.commandWorked()包裹命令。其实现位于 assert.js,核心逻辑:
- 对写结果类型(
_isWriteResultType)递归调用assert.writeOK()检查写错误; - 对
WriteCommandError或带ok字段的原始命令响应,检查ok === 1且无写错误; - 失败时构造包含
{res, originalCommand, connection}的失败消息,便于定位“哪条命令、在哪个连接上失败”; - 遇到写关注超时或非瞬态锁超时,会自动调用
MongoRunner.runHangAnalyzer()触发挂起分析器辅助定位。
与之配套的还有assert.commandFailed()(不校验错误码)以及更严格的assert.commandFailedWithCode()。项目建议优先使用后者,以免测试在意外错误码上“假通过”。从源码看,commandFailedWithCode会断言返回的错误码与期望码一致,见 assert.js 附近assert._kAnyErrorCode的用法。
2.3 遗留批量写 API:assert.writeOK()
规范要求在使用遗留的 bulk write API时使用assert.writeOK()。其实现见 assert.js,默认检查ok与writeErrors,支持通过{ignoreWriteConcernErrors}选项忽略写关注错误。当assert.commandWorked()接收到写结果对象时,也会内部转发到assert.writeOK(),二者在写操作路径上是同一套检查逻辑。
2.4 异步条件:assert.soon()
assert.soon()只用于异步或最终一致(eventually-consistent)的条件。其签名(见 assert.js):
assert.soon(func, msg, timeout, interval = 200, {runHangAnalyzer = true} = {}, attr)func返回真值即成功,否则每interval毫秒重试,直到超过timeout后失败;msg可为函数以便失败时动态生成消息。assert.soonNoExcept()则吞掉函数抛出的异常继续重试,适合在副本集主从切换等场景下轮询状态。使用这类断言时要注意:硬编码的超时值应设置合理上限(实践中常取 10 分钟),而不要随意选一个 30 秒的“魔数”。
2.5 常用断言族
除上述命令断言外,src/mongo/shell/README.md 总结了完整断言族,写作时按需选用:
- 比较类:
assert.eq/neq、docEq、setEq、sameMembers、close、closeWithinMS、between等; - 包含类:
hasFields、contains、doesNotContain、includes等; - 重试超时类:
soon、soonNoExcept、retry、retryNoExcept、time; - 异常类:
throws、throwsWithCode、doesNotThrow; - 命令类:
commandWorked、commandFailed、commandFailedWithCode、writeOK、writeError、writeErrorWithCode。
所有专用断言都接受msg与attr两个可选参数,因此都遵循下文的 JSON 日志规范。
三、Mocha 风格:新测试文件的标准组织方式
规范要求新添加的测试文件使用 mocha 风格组织用例。mongo shell 无法直接运行标准 Mocha,因此仓库提供了 jstests/libs/mochalite.js,一个面向 shell 的 Mocha 子集实现,提供了describe、it、before、beforeEach、afterEach、after六种 API。
3.1 基本结构
按需导入所需 API(规范强调“只导入需要的”,保持测试上下文干净):
import {before, describe, it} from "jstests/libs/mochalite.js"; describe("feature under test", function () { before(function () { /* one-time setup */ }); it("does X", function () { /* ... */ }); });jstests/README.md中给出了更完整的示例,展示了四个钩子与多个it的组合:
import {after, afterEach, before, beforeEach, describe, it} from "jstests/libs/mochalite.js"; describe("simple inserts and finds", () => { before(() => { this.fixtureDB = startupNewDB(); }); beforeEach(() => { this.fixtureDB.seed(); }); afterEach(async () => { await this.fixtureDB.clear(); }); after(() => { this.fixtureDB.shutdown(); }); it("should do something", () => { this.fixtureDB.insert({name: "test"}); assert.eq(this.fixtureDB.find({name: "test"}).count(), 1); }); it("should error on invalid data", () => { const e = assert.throws(() => this.fixtureDB.insert({notafield: undefined})); assert.eq(e.message, "Field 'notafield' not found"); }); });3.2 源码级实现原理
从 mochalite.js 的源码可以看清其运行模型:
- 组合模式的作用域树:
DescribeScope是复合节点,可嵌套其他DescribeScope或叶子节点TestScope(见 mochalite.js)。每个Scope共享一个Context实例,this贯穿整个套件。 - 两阶段 discover:
addDescribe只把子作用域挂到树中,真正执行fn()收集钩子与子用例发生在discover()(Phase 1 收集当前层钩子、Phase 2 递归发现子 describe)。这样保证嵌套 describe 能继承父级完整的beforeEach/afterEach,包括在嵌套 describe 调用之后注册的钩子(mochalite.js)。 - 钩子语义:
before/after围绕整个作用域执行一次;beforeEach按“外层先、内层后”顺序执行,afterEach按“内层先、外层后”执行。任一钩子失败都会记入 Reporter 并影响后续执行。 - 异步支持:
before/beforeEach/afterEach/after与it的内容都可以是 async 函数(或返回 Promise),框架会await其完成(mochalite.js)。注意钩子与it的函数不能声明参数(assertNoFunctionArgs会直接抛错),需要回调式写法时请改用 async 函数。 - 失败聚合:
Reporter不会在单个用例失败时立刻退出,而是聚合所有通过/失败用例,最后在report()中打印摘要;只要有失败,就抛出Error("N failing tests detected")让 shell 进程以失败退出,并把每个断言的attr(即error.extraAttr)原样转发给jsTest.log.error(mochalite.js)。
3.3 it.only / describe.only / it.skip / describe.skip
调试单个用例时,无需改文件结构,直接使用限定符:
it.only("should do something", () => { this.fixtureDB.insert({name: "test"}); assert.eq(this.fixtureDB.find({name: "test"}).count(), 1); });源码中的Scope.run()会对子节点做“only 过滤”:优先保留直接it.only,其次是与describe.only祖先匹配的作用域(mochalite.js);it.skip/describe.skip则实现为空操作,从执行树中剔除。
更推荐的做法是不改文件,通过 resmoke 的--mochagrep参数过滤。mochalite 在初始化时读取全局_mocha_grep并用正则匹配用例的完整标题(fullTitle()由父子 describe 标题用" > "拼接而成,见 mochalite.js):
buildscripts/resmoke.py run --suites=no_passthrough --mochagrep "do something" jstests/noPassthrough/mytest.js这镜像了 Mocha 的--grep能力,便于在 CI 或本地精确定位特定用例。
3.4 模块化最佳实践
仓库已全面迁移到 ES 模块世界,新测试必须使用模块与新风格,相关要求如下(详见 jstests/README.md):
- 只 import/export 所需符号,避免命名冲突(例如避免导出
alphabet这种过于通用的名字); - 变量声明优先
let/const,有助于尽早发现重复声明; - 导出使用 ES6 风格(
export function ...),让语言服务器支持代码导航; - 注意作用域:模块化后全局变量不会污染其他测试文件,比旧的
load()机制更安全。
四、日志输出:使用 attr 传递 JSON 兼容的结构化对象
规范要求在断言与日志函数中,把对象传给attr参数,而不是把tojson()拼接进消息字符串。绝大多数assert.*函数都接受attr对象作为最后一个参数。
4.1 为什么不能用 tojson 拼接
tojson()的返回值并不总是合法 JSON(README 明确说明其“不应被用于日志”),拼接进字符串后:结构化信息丢失、字段无法被日志系统索引检索、嵌套对象难以阅读。而attr参数会:
- 在 JSON 日志模式下,作为结构化字段随日志输出;
- 在断言失败时,随错误对象以
extra字段保留; - 支持
msg中的{key}占位符替换(formatErrorMsg实现,见 assert.js)。
4.2 正反示例
// Bad — tojson() in the message string assert(cursor.hasOwnProperty("metrics"), "metrics missing: " + tojson(cursor)); // Good — object passed via attr assert(cursor.hasOwnProperty("metrics"), "metrics missing", {cursor});日志同理:
// Bad jsTest.log.info("got cursor: " + tojson(cursor)); // Good jsTest.log.info("got cursor", {cursor});4.3 日志函数的完整形态
src/mongo/shell/README.md 列出了完整日志函数族:jsTestLog()、jsTest.log()、jsTest.log.error()、jsTest.log.warning()、jsTest.log.info()、jsTest.log.debug(),全部接受msg与可选的attr对象。在 JSON 日志格式下,一次失败的断言会输出带attr.extra字段的完整结构化日志(含originalError、stack、extra等),便于日志分析系统直接消费。
另外注意:mochalite 的 Reporter 使用jsTest.log.info/jsTest.log.error输出通过/失败信息,并专门把断言的attr转发为结构化日志数据而非丢弃(mochalite.js),这保证 mocha 风格下的失败用例同样保留结构化上下文。
五、资源与设置清理:测试隔离的底线
规范第四条:ALWAYS 在资源不再需要时立即清理。示例资源包括服务器端的游标(cursor)、集合(collection);示例设置包括服务器参数(server parameters)与测试框架存储在全局TestData对象中的设置。
5.1 为什么必须清理
jstests/README.md的“Test Isolation”一节给出了根本原因:
- 你的测试文件通常与其他成百上千个测试文件共享同一套 fixture(CI 中 resmoke 使用
--continueOnFailure,fixture 直到套件结束才销毁); - 因此测试必须从已知状态开始(start from a known state),并在结束时恢复该状态;
- 如果修改了 fixture,即使测试失败也要尽量安全还原,否则会污染后续测试文件。
清理的推荐做法是使用 mocha 风格的after/afterEach钩子。例如after(() => { this.fixtureDB.shutdown(); })、afterEach(async () => { await this.fixtureDB.clear(); })。钩子即使在前置断言失败时也会按框架流程执行,天然适配“尽力还原”的要求。
5.2 环境前提用 @tags 而非提前 return
规范还提醒:如果测试对环境有前置条件(如需要特定 feature compatibility version),应当使用@tags声明而不是在测试里提前 return,这样套件调度阶段就能排除不支持的环境,比“测试运行时才发现不支持”高效得多。示例:
/** * Tests for the XYZ feature * @tags: [requires_fcv_81] */5.3 相关的其他隔离原则
配合上述清理要求,仓库还强调了若干测试设计原则(详见 jstests/README.md):
- 最小化用例:只保留验证目标行为所必需的步骤,让新人一眼看懂;
- 顶层注释:文件头部用块注释清晰说明测试验证什么,复杂测试可补充步骤说明;
- 可调试性:断言错误消息应包含服务器响应等全部排障信息;不要插入无关的相同文档(便于溯源数据来源);重复逻辑抽到公共库;
- 不要硬编码库名/集合名:用描述性变量名(如
collectionToDrop优于collName); - 确定性优先:能用 failpoint 固定事件顺序就不要依赖时序;fuzzer 与并发套件是例外;
- 尽早失败:每个命令都要包裹
assert.commandWorked或assert.commandFailedWithCode,避免错误后置放大; - 避免间接断言:断言最具体的属性(如集合精确名称存在),而不是集合计数等于 1。
六、实战检查清单
综合 jstests/AGENTS.md 与 jstests/README.md,写一个新的 jstest 时可对照以下清单自检:
- 文件头部有块注释说明测试目的(复杂场景补充步骤概述);
- 使用 mocha 风格(
describe/it/ 钩子),只 import 所需符号,优先let/const; - 每个命令都用
assert.commandWorked()包裹;预期失败用assert.commandFailedWithCode();遗留 bulk API 用assert.writeOK(); - 异步/最终一致条件才用
assert.soon(),超时值设置合理; - 日志与断言消息传递对象时走
attr参数,不用tojson()拼接; - 游标、集合等资源与服务器参数、
TestData设置用完立即清理,优先用after/afterEach钩子; - 环境前提用
@tags声明而非提前 return; - 不做间接断言、不硬编码库名/集合名、保持用例最小化与确定性。
按此清单产出的测试,既符合 MongoDB 服务器仓库的长期维护要求,也最容易在--mochagrep过滤、JSON 日志分析和失败排查等场景中被快速定位与调试。
【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考