Mocha — 只负责跑测试的可组合 runner
已复核Mocha 是一个只负责发现、组织并执行测试的 JavaScript runner。日常类比:它是考试监考员,不是试卷印刷厂——谁出题(断言)、谁扮假人(mock)、谁统计分数(覆盖率)都由你另选。
const { describe, it } = require('mocha')const assert = require('node:assert')
describe('sum', () => { it('adds', () => { assert.equal(1 + 1, 2) })})固定 11.8.0 仍是 CommonJS 包("type": "commonjs"),默认 UI 是 BDD:describe / it / before / after。它不绑定 Chai 或 sinon;Node 自带 assert 就能跑通。
不理解 Mocha 和 jest 的分工,就很难解释:
- 为什么很多老 Node 仓库是
mocha + chai + sinon + nyc,而不是一个包 - 为什么默认「一个进程跑完全部文件」——模块缓存和全局变量会串
- 为什么
--parallel不是 worker thread,而是子进程池 - 为什么
it('x', function(done) { ... })会被当成异步:看的是fn.length
固定源码的主链可以拆成五步:
-
构造 Mocha 实例:有限状态机
init → running → referencesCleaned | init → disposed。默认从mocharc.json读timeout=2000、slow=75、reporter=spec、ui=bdd。 -
登记文件与接口:BDD 在
EVENT_FILE_PRE_REQUIRE把describe/it挂到 context;每个it变成Test,再挂进Suite树。 -
决定怎么加载文件:默认
run()先loadFiles(),同一进程require/import。watch、并行或 ESM lazy 路径会推迟加载。 -
串行执行:默认
Runner自己遍历 Suite,按beforeAll → beforeEach → test → afterEach → afterAll调Runnable。Runnable默认_retries = -1(不重试)。 -
可选并行:
--parallel且jobs未设或>1时,换成ParallelBufferedRunner。它不执行 Runnable,只把文件丢给workerType: "process"的子进程池;默认maxWorkers = cpus - 1。
案例 1:最小 CJS 套件
Section titled “案例 1:最小 CJS 套件”mkdir mocha-toy && cd mocha-toynpm init -ynpm i -D mocha@11.8.0test/sum.test.js:
const assert = require('node:assert')
describe('sum', () => { it('1 + 1 = 2', () => { assert.equal(1 + 1, 2) })})npx mocha 默认收 js/cjs/mjs。这里没有 expect,因为 Mocha 不提供断言库。
案例 2:done-callback 与 Promise 是两条异步合同
Section titled “案例 2:done-callback 与 Promise 是两条异步合同”it('calls done', function (done) { setTimeout(() => done(), 10)})
it('returns a promise', async () => { await Promise.resolve()})Runnable 用 fn.length 判断第一种:有形参就走 done。箭头函数没有自己的 arguments/this,也没有形参时会被当成同步;超时默认 2000ms,可用 this.timeout(0) 关掉(会被 clamp 到 0 或 2^31-1)。
案例 3:并行是子进程,不是线程
Section titled “案例 3:并行是子进程,不是线程”npx mocha --parallel --jobs 2parallelMode(true) 会换 Runner 类并打开 lazy load。buffered-worker-pool.js 写明 workerType: "process",把当前 process.execArgv 传给子进程。globalSetup / globalTeardown 不会序列化进 worker。浏览器环境会直接抛「parallel mode is only supported in Node.js」。
-
默认同进程共享
require.cache:文件 A 改掉单例,文件 B 可能看到脏状态。要隔离得--parallel、自行 unload,或别依赖模块级可变状态。 -
把
--parallel想成 worker thread:固定实现是 child process。注释里的「worker」指池里的工作进程。 -
fn.length误判异步:it('x', function(done) {})才是 done 风格;it('x', () => { done() })若没声明形参,Mocha 当同步,done还没调用测试就结束了。 -
重试默认不存在:
_retries初始是-1。要重跑必须显式this.retries(n)或 CLI--retries。 -
复用同一 Mocha 实例:
run()之后若cleanReferencesAfterRun已把状态推到referencesCleaned,再run()会抛 disposed 错误,需要新实例。
适用 vs 不适用场景
Section titled “适用 vs 不适用场景”适用:
- 希望自己选断言 / mock / 覆盖率,而不是接受全家桶
- 已有 mocha 生态配置(nyc、chai、sinon、自定义 reporter)
- 满足当前 engines:
^18.18.0 || ^20.9.0 || >=21.1.0
不适用:
- 想要开箱即用的 expect、automock 和 snapshot——那是 jest 的范围
- 浏览器里开
--parallel——源码直接拒绝 - 把「一个进程跑完全部文件」当成隔离保证
固定版本边界
Section titled “固定版本边界”- 本文绑定
mochajs/mocha@90c1bb3e183a262ac91d83fa45035d03ea9f6045,tag 与 npmgitHead均为11.8.0。 - 默认 timeout 2000ms、slow 75ms、reporter
spec、UIbdd。 - 并行默认
cpus - 1个子进程;jobs === 1不会进入 parallelMode。 - ESM 加载走
requireOrImport:有process.features.require_module时优先require,.mjs仍用import()。 - 本文未安装依赖、运行上游测试或测量并行加速,状态保持
UNVERIFIED。
- Runner 可以不做断言——组合带来灵活,也把隔离和工具链交给调用方。
- 默认同进程是功能,也是陷阱——
require.cache是隐式共享内存。 - 并行实现必须看 workerType——名字叫 worker,源码却 fork 进程。
- 异步合同写在函数签名上——
fn.length比「看起来像 async」更硬。
- 默认不传
--parallel时,两个测试文件是否一定在不同进程? it('x', function(done) {})为什么会被当成异步?--parallel --jobs 1会启用ParallelBufferedRunner吗?
检查点:
- 不一定。默认同一进程
loadFiles()。 - 因为
Runnable看fn.length,有形参就走 done。 - 不会。源码要求
jobs未定义或>1才parallelMode(true)。
- 官方文档:mochajs.org
- 固定源码:mochajs/mocha —— 本文绑定提交
90c1bb3e183a262ac91d83fa45035d03ea9f6045 - 共享审查记录:
docs/test-runner-source-review-20260827-ag.md - jest —— 全家桶对照:默认 circus + expect + mock
- testing-library —— 只提供查询,仍需要 runner
- jest —— 一体化对照模型
- testing-library —— DOM 查询层,常挂在 Mocha 或 Jest 上
- msw —— 网络 mock,不依赖某个 runner
- yargs —— 许多 CLI(含历史 mocha 栈)用它做参数解析