MongoDB 测试指南:使用 resmoke 测试编排器运行与配置集成测试
【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo
resmoke 是 MongoDB 仓库内置的测试运行器与编排工具,几乎所有 MongoDB 测试(尤其是jstests/下的 JavaScript 集成测试)都经由它调度执行。本文以 docs/testing/README.md 为主线,结合仓库内的 suites、fixtures、hooks、testcases 等配套文档与源码实现,系统讲解 resmoke 的四大核心概念(suites、fixtures、hooks、testcases)、从零跑通一个测试的完整流程,以及如何读懂并编写自定义测试套件配置。读完本文,你将能够独立使用 resmoke 运行指定测试文件、解释 suite YAML 的每个字段含义,并知道如何按需组合拓扑(fixture)与测试前后逻辑(hook)。
1. resmoke 是什么
resmoke 是 MongoDB 的集成测试运行器(integration test runner),其入口文件位于仓库根目录下的 buildscripts/resmoke.py。该脚本本身只是一个薄壳,主要作用是修正PYTHONPATH后调用buildscripts.resmokelib.cli模块(对应 buildscripts/resmokelib/cli.py)的main()函数,再由parser.parse_command_line()解析子命令并执行。
resmoke 负责的工作远不止“执行一条命令”这么简单,它实际上是一个测试编排器(orchestrator),需要协调:
- 按 suite 配置挑选(select)要运行的测试文件;
- 按 fixture 配置拉起对应的服务器拓扑(如独立 mongod、副本集、分片集群);
- 按 hooks 配置在测试前后执行数据校验、清理等附加逻辑;
- 汇总测试结果、在失败时归档数据文件(archive)供事后分析。
值得注意的是,尽管 MongoDB 源码本身使用 Bazel 构建,但 resmoke尚未与 Bazel 集成,因此运行测试前需要先用bazel build产出可执行二进制(详见 buildscripts/resmokelib/README.md 的 Build 一节)。
2. 四大核心概念
原文档明确将 resmoke 的知识体系拆成四个独立概念,每个都有自己专门的文档。它们是理解 resmoke 的钥匙:
- suites(套件):决定“跑哪些测试、怎么跑”,本质是 YAML 配置文件。详见 buildscripts/resmokeconfig/suites/README.md;
- fixtures(夹具):指定测试所针对的服务器拓扑(独立节点、副本集、分片集群等)。详见 buildscripts/resmokelib/testing/fixtures/README.md;
- hooks(钩子):在单个测试之前、之后以及整个 suite 前后运行的逻辑(如校验数据一致性、周期性清理)。详见 buildscripts/resmokelib/testing/hooks/README.md;
- testcases(测试用例类型):resmoke 可以运行的不同“种类”的测试,本质是 Python
unittest.TestCase的扩展。详见 buildscripts/resmokelib/testing/testcases/README.md。
下面分别展开。
2.1 Suites:测试如何被分组与配置
Suite 是一份 YAML 文件,文件名即 suite 名,其作用是把“要跑的测试文件、fixture 及其参数、shell 选项、hooks、归档策略”打包成一个可复用的配置单元。所有 suite 配置位于 buildscripts/resmokeconfig/suites/ 目录(约 940 个 YAML 文件)。
一个最精简的 suite 只需要三部分:
test_kind: js_test selector: roots: - jstests/mytests/**/*.js executor: config: shell_options: nodb: ""含义是:suite 名即文件名my_suite;包含jstests/mytests目录下所有 JS 文件;这些测试在一个传入nodb: ""选项的 shell 中运行。
下面用一个带占位符的完整结构示例展示 suite 的整体骨架(摘自 buildscripts/resmokeconfig/suites/README.md):
test_kind: js_test selector: roots: - jstests/mytests/**/*.js executor: config: shell_options: nodb: "" global_vars: TestData: defaultReadConcernLevel: null hooks: - class: ValidateCollections - class: CleanEveryN n: 20 fixture: class: ShardedClusterFixture num_shards: 2 archive: tests: true hooks: - ValidateCollections2.1.1test_kind
声明该 suite 中测试的种类,常见值如js_test、cpp_unit_test、py_test、fsm_workload_test等。全部受支持的类型见 buildscripts/resmokelib/testing/testcases/README.md。其中js_test覆盖了大约75% 的 suite,是绝对主力。
2.1.2selector:选择测试文件
selector 决定 suite 包含/排除哪些测试文件,主要字段:
roots:要包含的测试文件路径列表,支持 glob。如果提供的是不带 glob 的路径,则该文件必须真实存在;root:一个文件(通常为build/unittests.txt),内部每行一个 glob 模式,常用于cpp_unit_test类型,指定候选测试;include_files:glob 列表,只保留这部分测试;即使这些文件被 tag 排除了也会被强制包含;若指定了 roots 中不存在的测试会报错;exclude_files:glob 列表,排除这些测试;即使 tag 允许也会被排除;若指定的测试不在 roots 中会报错;include_with_any_tags:只保留定义了任一这些 tag 的 jstest;exclude_with_any_tags:排除定义了任一这些 tag 的 jstest(除非文件名层面被强制包含)。
示例如下:
selector: roots: - jstests/aggregation/**/*.js exclude_files: - jstests/aggregation/extras/*.js - jstests/aggregation/data/*.js exclude_with_any_tags: - requires_pipeline_optimization想查看仓库中所有被引用的 tag 及其说明,可运行./buildscripts/resmoke.py list-tags。
2.1.3executor:定义如何执行
executor下分四块:config、hooks、fixture、archive。
executor.config提供每个测试的附加配置,结构随test_kind变化(具体以 buildscripts/resmokelib/testing/testcases/ 下对应实现为准)。对最常见的js_test而言,核心是shell_options:
config: shell_options: global_vars: TestData: defaultReadConcernLevel: null nodb: "" gssapiServiceName: "mockservice" eval: >- var testingReplication = true; load('jstests/libs/override_methods/set_read_and_write_concerns.js'); load('jstests/libs/override_methods/enable_causal_consistency_without_read_pref.js');shell_options中除global_vars外的参数会原样传给 mongo shell 可执行文件;标志型参数需要写成''空值,如nodb: "";global_vars作为传给--eval的字符串基础(会做对象格式化),eval字段指定的 JS 会追加在其后;TestData是一个特殊的全局变量,用于承载测试数据。它的取值合并优先级为:resmoke 命令行 > suite.yml> 运行时/默认值;eval可以直接写 JS 代码,也可以把 JS 放进独立脚本后用load(...)加载。
executor.hooks声明要在测试前后运行的钩子。yml 中的class必须与 hook 的 Python 类名一致,其余字段作为参数传入构造函数(hook_logger与fixture由 resmoke 自动注入,不要写在 yml 里)。示例:
hooks: - class: CheckReplOplogs - class: CheckReplDBHash - class: ValidateCollections - class: CleanEveryN n: 20 - class: MyHook param1: something param2: somethingelseexecutor.fixture指定测试所运行的拓扑。class对应 fixture 的 Python 类名,其余字段传给构造函数:
fixture: class: ShardedClusterFixture num_shards: 2 mongos_options: bind_ip_all: "" set_parameters: enableTestCommands: 1 mongod_options: bind_ip_all: "" set_parameters: enableTestCommands: 1 periodicNoopIntervalSecs: 1 writePeriodicNoops: trueexecutor.archive配置失败时的数据归档(上传到 S3)。当某个 hook 或 test 抛异常即视为失败,触发归档的时机有二:归档列表中的任意 hook 抛异常;或tests: true时 suite 中任意 test 抛异常。示例:
archive: hooks: - Hook1 - Hook2 tests: truearchive.hooks是 hook 类名列表(设为true表示归档所有 hook);archive.tests是测试文件列表(支持通配,true表示归档所有测试)。关于失败归档的更多机制可阅读 buildscripts/resmokeconfig/suites/README.md 与 buildscripts/resmokelib/testing/testcases/README.md 中对FixtureAbortTestCase的说明。
2.2 Fixtures:测试针对的服务器拓扑
Fixture 定义测试所针对的具体拓扑,在 suite 的fixture字段中声明。仓库支持的 fixture 包括(均位于 buildscripts/resmokelib/testing/fixtures/):
MongoDFixture(standalone.py):提供独立 mongod;ReplicaSetFixture(replicaset.py):副本集;ShardedClusterFixture(shardedcluster.py):分片集群,同时承担“由 JS 测试自身通过MongoRunner、ReplSetTest、ShardingTest启动部署”的场景;MultiReplicaSetFixture(multi_replica_set.py)、MultiShardedClusterFixture(multi_sharded_cluster.py):多组副本集/分片集群;BulkWriteFixture(bulk_write.py):向 JSTest 提供一组集群;ExternalFixture(external.py)与ExternalShardedClusterFixture:连接外部(非 resmoke 管理)的集群;MongoTFixture(mongot.py):mongod 旁边再拉起一个 mongot;YesFixture(yesfixture.py):生成大量日志消息的辅助 fixture。
fixture 侧还有一组接口类(interface.py):Fixture是所有 fixture 的基类;MultiClusterFixture是可由多个独立集群组成的基类(参与者集群平时独立运行,仅在参与迁移等过程时被绑定);NoOpFixture不启动任何服务器;ReplFixture是所有支持复制的 fixture 的基类。
2.3 Hooks:测试边界上的运行逻辑
Hook 是在测试内容边界(before/after test、before/after suite)上运行的例程。suite 的hooks字段中可声明任意组合。仓库支持的 hooks 数量很多,按用途可粗略分为几类(详见 buildscripts/resmokelib/testing/hooks/README.md):
- 数据一致性校验:如
CheckReplDBHash(dbhash.py)对比主从 dbhash;CheckReplOplogs(oplog.py)检查local.oplog.rs在主备上一致;CheckReplPreImagesConsistency检查config.system.preimages;ValidateCollections(validate.py)做完整校验;CheckOrphansDeleted(orphans.py)检查孤儿文档是否删除干净; - 后台扰动/故障注入:
ContinuousStepdown(stepdown.py)周期性发送replSetStepDown;PeriodicKillSecondaries(periodic_kill_secondaries.py)周期性杀掉副本集 secondary;LagOplogApplicationInBackground(secondary_lag.py)制造 secondary 回放延迟;FuzzRuntimeParameters(fuzz_runtime_parameters.py)周期性下发随机setParameter;HelloDelays(hello_failures.py)注入 Hello 故障; - 初始同步相关:
BackgroundInitialSync/IntermediateInitialSync(initialsync.py),使用前提是 ReplicaSetFixture 以start_initial_sync_node=True启动,且与CleanEveryN共用时n要一致; - 清理与重启:
CleanEveryN(cleanup.py)每跑n个测试重启 fixture;DropUserCollections、CleanupConcurrencyWorkloads(cleanup_concurrency_workloads.py); - 变更流/后台任务:
RunChangeStreamsInBackground(change_streams.py)在后台跑全集群 change stream;RunDBCheckInBackground(dbcheck_background.py)后台跑dbCheck;RunQueryStats(run_query_stats.py)每个测试后运行$queryStats; - 崩溃模拟与归档:
SimulateCrash(simulate_crash.py);MagicRestoreEveryN(magic_restore.py)依赖MagicRestoreFixture。
所有 hooks 都继承自buildscripts.resmokelib.testing.hooks.interface.Hook(interface.py),并可覆写以下空方法中的任意子集:
before_suitebefore_testafter_testafter_suite
至少要覆写其中一个方法,否则 hook 什么都不做。常见的自定义工作包括校验数据、删除数据、执行清理等。此外还有三个接口层:JSHook(jsfile.py)是携带静态 JS 文件的 hook 接口,DataConsistencyHook是其上用于数据一致性检查的封装(shell 以非零码退出时抛errors.ServerFailure终止测试);BGHook(bghook.py)会在后台线程中反复调用run_action(),贯穿整个 suite 生命周期;PerClusterDataConsistencyHook则在 fixture 的每个独立集群上运行。
2.4 Testcases:测试的“种类”
TestCases 是 Pythonunittest.TestCase的扩展,resmoke 以不同“种类”(test_kind)运行它们。完整清单见 buildscripts/resmokelib/testing/testcases/README.md,此处列举代表性类型:
js_test(jstest.py):JS 集成测试,约 75% 的 suite 使用,具体编写规范见 jstests/README.md;all_versions_js_test:js_test的别名,用于多版本(multiversion)透传 suite,会以副本集/分片集群的所有版本组合运行;cpp_unit_test(cpp_unittest.py):C++ 单元测试;cpp_integration_test、cpp_libfuzzer_test对应 C++ 集成与 libfuzzer 测试;fsm_workload_test/parallel_fsm_workload_test(fsm_workload_test.py):并发(FSM)工作负载测试;py_test(pytest.py):Python 测试;benchmark_test、json_schema_test、sdam_json_test、server_selection_json_test、sleep_test、tla_plus_test等各司其职。
接口层面,顶层有TestCase(必须实现run_test)与ProcessTestCase(执行外部进程,必须实现_make_process)(均在 interface.py);子类包括JSRunnerFileTestCase(jsrunnerfile.py)与MultiClientsTestCase(多个单用例副本的封装)以及TestCaseFactory工厂。
一个值得注意的机制是Fixture TestCases:resmoke 内部通过FixtureTestCaseManager用测试用例来协调 fixture 生命周期——suite 会先跑一个FixtureSetupTestCase建好拓扑,再跑你的 N 个测试,最后跑FixtureTeardownTestCase拆除。因此一次运行中你会看到N+2个“测试”通过,多出的两个正是 fixture 的 setup 与 teardown。另外,当测试失败且配置了归档时,resmoke 会动态生成一个FixtureAbortTestCase立即执行,向每个 mongod 发送SIGABRT以便在归档前捕获崩溃现场。
3. 动手实践:从零跑通一个测试
3.1 准备 Python 虚拟环境
先确保 venv 已激活且依赖最新:
python3 -m venv python3-venv source python3-venv/bin/activate buildscripts/uv_sync.shuv_sync.sh是仓库提供的依赖同步脚本,位于 buildscripts/uv_sync.sh,它会按仓库锁定的版本安装 Python 依赖。如果直接运行buildscripts/resmoke.py而 venv 未激活(且不在 Bazel workspace 环境下),buildscripts/resmoke.py 会提示 "You need to activate your virtual environment"。
3.2 构建被测二进制
由于 resmoke 尚未与 Bazel 集成,需要先用 Bazel 构建可测试的安装产物:
bazel build install-dist-test3.3 运行单个测试文件
以原文档中的示例为例,从单个测试文件运行测试内容:
buildscripts/resmoke.py run --suites=no_passthrough jstests/noPassthrough/shell/js/string.js这条命令做了这些事:
- 通过
run子命令(resmoke 最常用的功能)执行测试; --suites=no_passthrough指定使用 buildscripts/resmokeconfig/suites/no_passthrough.yml 中的 suite 配置;- 命令行给出的 JS 文件 jstests/noPassthrough/shell/js/string.js 必须落在该 suite 的
roots通配范围内。
no_passthroughsuite 的特点是:“passthrough”指用不同的运行时集群配置(拓扑、运行时参数、故障注入等)来跑同一测试,大多数测试都能在 passthrough suite 中运行;而 noPassthrough 是例外——这些测试只在测试自身预定义的精确配置下运行(见 buildscripts/resmokeconfig/suites/no_passthrough.yml 的description)。它的selector.roots通过 glob 覆盖了jstests/noPassthrough/**/*.js、jstests/concurrency/*.js等目录,exclude_files排除了jstests/noPassthrough/libs/**/*.js这类库文件,并通过exclude_with_any_tags排除requires_kernel_619、requires_kernel_7014等需要特定内核的测试。
它的 executor 配置非常精简——原文档明确指出:不指定 fixture、不指定 hooks,executor 只带一份最小配置(nodb: ""让 shell 以不连接数据库的模式启动,并关闭测试扩展签名校验的 server parameter)。这正说明这些 noPassthrough 测试由 JS 代码自己启动 mongod(MongoRunner等),suite 层无需再管理拓扑。
被执行的 jstests/noPassthrough/shell/js/string.js 是一个用 mochalite 编写的 String shim/polyfill 测试,包含trim、trimLeft、trimRight、ltrim、rtrim、startsWith、endsWith、includes、pad等用例——非常适合作为入门跑通流程的最小样例。
3.4 run 子命令与常用参数
run子命令有100+ 个 flag。resmoke 刻意不在文档中重复罗列全部参数,以免多源信息漂移过期,统一以buildscripts/resmoke.py run --help为准。以下是高使用频率参数的说明(源自 buildscripts/resmokelib/README.md):
--suites/--suiterun既可以运行 suite(一组测试 + 对应的拓扑与配置),也可以运行显式指定的测试文件。单套件用--suite,多套件用逗号分隔传给--suites。
--installDirresmoke 可以针对任意“可测试安装”(ASAN、Debug、Release 等)运行测试。当本地构建被安装到 git 仓库根目录的子目录、且仓库内恰好只有一份构建时,resmoke 能自动定位并使用本地构建;其他情况则用--installDir显式指定 mongod/mongos 二进制所在目录。替代方案是直接使用与 mongod 二进制同目录下的resmoke.py包装脚本,它会自动替你设置installDir。注意:该包装脚本在打包安装(如 Homebrew 等包管理器提供的发行版)中不存在,此时必须显式传--installDir。
其他子命令(摘录自resmoke --help):
list-suites:列出可执行的 suite 名;find-suites:列出会执行指定测试的 suite;generate-matrix-suites:从映射文件生成 matrix suite 配置;list-tags:列出 suites 中可用的 tag 及说明;generate-multiversion-exclude-tags:基于当前分支与 last-lts/last-continuous 分支上BACKPORTS_REQUIRED_FILE的对比,生成多版本测试的排除 tag 文件;test-discovery:发现 suite 会运行哪些测试;suiteconfig:显示 suite 的配置;core-analyzer:分析指定输入文件的 core dump;hang-analyzer:Evergreen 集成的原型挂起分析器,支持抓取 dump 与进程信息摘要;powercycle:断电循环测试脚本;generate-fuzz-config:用配置模糊器生成 mongod.conf 与 mongos.conf;generate-fcv-constants等其他内部命令。
另外注意:bisect、setup-multiversion、symbolize三个子命令已迁移到db-contrib-tool工具,不再由 resmoke 提供。
4. 深入原理:resmoke 的命令行入口与参数校验
从源码看,resmoke 的入口链路是:
- buildscripts/resmoke.py 修正
PYTHONPATH后调用cli.main(sys.argv); - buildscripts/resmokelib/cli.py 的
main()记录进程号到环境变量(RESMOKE_PARENT_PROCESS、RESMOKE_PARENT_CTIME,供子进程识别父进程),若由bazel run调用则切到 workspace 根目录,随后交给parser.parse_command_line()解析并执行对应子命令; - 参数解析与校验位于
buildscripts/resmokelib/parser模块与 buildscripts/resmokelib/configure_resmoke.py 的_validate_options()中。其中可以看到若干对用户友好的校验逻辑,例如:--executor已被--suites取代,若使用会直接报错并提示改用--suites={} {};- 命令行测试文件列表与
--replayFile不能同时使用; --shellSeed必须配合且仅配合一个测试文件使用;- 指定的测试文件若不存在(且不以
@开头表示 replay 文件)会报 "Test file ... does not exist"。
5. 遥测:resmoke 的 OpenTelemetry 埋点
resmoke 使用 OpenTelemetry(OTel)采集自身运行的指标,用于 Evergreen CI 场景下的性能优化。每次 resmoke 调用都会采集,但只有运行在 Evergreen 时数据才会上报到 Honeycomb(详见 docs/testing/otel_resmoke.md)。采集内容包括:
- 一个 resmoke suite(一组 JS 测试)的运行时长;
- suite 内每个测试(单个 JS 测试)的运行时长;
- 测试/suite 前后 hooks 的耗时;
- resmoke 归档器(失败时归档 core dump)的相关数据。
实现上,大部分配置集中在_set_up_tracing(...)方法(buildscripts/resmokelib/configure_resmoke.py),并配套了BatchedBaggageSpanProcessor(buildscripts/resmokelib/utils/batched_baggage_span_processor.py)与FileSpanExporter(buildscripts/resmokelib/utils/file_span_exporter.py)两个自定义组件。数据采集以装饰器为主,例如(取自 buildscripts/resmokelib/testing/job.py):
TRACER = trace.get_tracer("resmoke") @TRACER.start_as_current_span("func_name") def func_name(...): span = trace.get_current_span() span.set_attribute("attr1", True)装饰器方式可以自动捕获异常、保证 span 一定被关闭;个别场景也会用with块手动开 span,但装饰器是首选方案。
6. 相关文档导航
- 测试运行器总览:buildscripts/resmokelib/README.md
- Suite 配置详解:buildscripts/resmokeconfig/suites/README.md
- Fixture 与拓扑:buildscripts/resmokelib/testing/fixtures/README.md
- Hook 机制:buildscripts/resmokelib/testing/hooks/README.md
- TestCase 种类:buildscripts/resmokelib/testing/testcases/README.md
- JS 测试编写规范:jstests/README.md
- resmoke 遥测:docs/testing/otel_resmoke.md
- 并发测试框架:docs/testing/fsm_concurrency_testing_framework.md
- 挂起分析:docs/testing/hang_analyzer.md
【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考