mocha review
Mocha 11.8.0 finds JavaScript test files, runs suites and hooks, applies timeouts and retries, and reports the outcome. Our Node 22 package check found a CommonJS runner with 21 direct dependencies, no bundled TypeScript declarations, and a browser entry that esbuild could not bundle. Mocha supplies BDD, TDD, exports, and QUnit-style interfaces along with serial or multi-process execution. Assertions, spies, module mocks, snapshots, transforms, and coverage are separate choices. Version 11.8.0 adds `--fail-hook-affected-tests`, which reports tests skipped after a hook failure as failed instead of leaving that consequence hidden in skipped counts.
Our Mocha 11.8.0 install took 4.7 seconds, occupied 13 MB across 76 packages, and reported 3 vulnerabilities plus 1 deprecation warning, so its best case is an established suite or a deliberately modular test stack. Start a new Node service with node:test, or compare Vitest when transforms and integrated mocking are required.
We installed it
| Install | ✓ · 4.7s | 76 packages on disk · 13 MB · 1 deprecation warning |
| Import | ✓ | ESM import works · require() works · CommonJS package |
| Browser | n/a | could not be bundled for the browser (Node-only code, most likely) |
| Types | — | no TypeScript types found |
| Known vulns | 3 | 0 critical · 1 high · 1 moderate · 1 low (npm audit) |
Answers from our run
Does mocha install cleanly?
Yes. In a fresh container with an empty cache, npm install mocha finished in 5 seconds, leaving 76 packages and 13 MB on disk. npm audit reported 3 known vulnerabilities. The install printed 1 deprecation warning.
Can mocha run in a browser?
Not directly: esbuild could not bundle it for the browser in our run, which normally means it depends on Node built-ins. Use it on the server, or find a browser-targeted alternative.
Does mocha work with both ESM and CommonJS?
Yes. Both import 'mocha' and require('mocha') worked in Node 22 in our run. The package is published as CommonJS.
Does mocha include TypeScript types?
No type declarations were found in our install, so TypeScript users need their own declarations.
mocha or vitest: which should you use?
vitest: Choose it for Vite-aware TypeScript transforms with integrated mocks, snapshots, and coverage. Our Mocha 11.8.0 install took 4.7 seconds, occupied 13 MB across 76 packages, and reported 3 vulnerabilities plus 1 deprecation warning, so its best case is an established suite or a deliberately modular test stack.
When should you not use mocha?
Node's built-in test runner already covers ordinary Node-only suites, assertions, mocking, concurrency, and coverage without Mocha's 76-package install
Use it if
- Mocha 11 can keep an established suite running while the application changes around it
- The team deliberately combines node:assert or Chai with Sinon and c8 instead of adopting one runner's complete testing stack
- Integration tests need hooks, timeouts, retries, selectable reporters, and explicit process behavior more than source transforms
- Root-hook plugins and .mocharc options must support several test packages with a shared runner policy
- Node's built-in test runner already covers ordinary Node-only suites, assertions, mocking, concurrency, and coverage without Mocha's 76-package install
- Vitest fits TypeScript, JSX, Vite transforms, snapshots, mocks, and coverage under one config; Mocha 11 ships neither a transform pipeline nor TypeScript declarations
- A team expecting the runner itself to provide fake timers, spies, module mocks, snapshots, and coverage will need several additional packages around Mocha
- Parallel mode conflicts with .only, --file, --sort, file-order assumptions, and some reporters, so a suite built on those behaviors cannot enable it safely
- Our install produced 3 audit findings, including 1 high severity, plus 1 deprecation warning and 13 MB on disk; a new low-dependency project should account for those facts
Setup reality
We installed Mocha 11.8.0 in 4.7 seconds in a fresh, unprivileged Node 22 Bookworm sandbox with 3 CPUs and 8 GB of RAM. npm printed 1 deprecation warning and left 76 packages occupying 13 MB. npm audit found 3 vulnerabilities: 1 high, 1 moderate, 1 low, and 0 critical. Mocha declares 21 direct dependencies, 0 peers, a 2,480 KB unpacked package, and Node ^18.18.0, ^20.9.0, or >=21.1.0.
The installed package is CommonJS with no exports map. require() and ESM import both succeeded, but no TypeScript declarations were present. Add @types/mocha plus a loader such as tsx for TypeScript discovery, and keep tsc --noEmit as its own check. Assertions can come from node:assert; spies and coverage still require other tools. Our esbuild browser build failed, so the installed entry is not a drop-in browser bundle.
Mocha takes CLI flags, one selected .mocharc source, or the mocha key in package.json. The default timeout is 2 seconds. Calls to this.timeout(), this.retries(), and this.skip() require function callbacks because arrow functions have no Mocha context. Databases, servers, and timers should close in teardown. --exit forces termination and can turn an open-handle defect into a passing 2-second shutdown.
Parallel mode starts test files in separate processes, so module state and file ordering cannot coordinate them. It rejects .only and conflicts with --file, --sort, and some reporters. Version 11.8.0's --fail-hook-affected-tests changes reporting after a failed hook by marking affected skipped tests as failures. Decide that CI policy explicitly; it can alter counts and failure output without executing one additional test.
Patterns
Run JavaScript specs under test install-and-run
npm install --save-dev mocha
// package.json
{
"scripts": {
"test": "mocha",
"test:watch": "mocha --watch"
}
}
# runs ./test/*.{js,cjs,mjs} by default
npm testMocha 11 checks JavaScript files directly under test by default; nested specs need --recursive or an explicit quoted glob.
Test a cart with node:assert write-a-test
import assert from 'node:assert/strict'
import { total } from '../src/cart.js'
describe('total', function () {
it('sums line items', function () {
assert.equal(total([{ price: 100, qty: 2 }]), 200)
})
it('rejects a negative quantity', function () {
assert.throws(() => total([{ price: 100, qty: -1 }]), /quantity/)
})
})node:assert/strict covers equality and thrown errors without adding another package to the 76-package install.
Finish async tests with a promise or done async-tests
it('loads the user', async function () {
const user = await getUser(1)
assert.equal(user.id, 1)
})
it('emits done once', function (done) {
emitter.once('ready', () => done())
emitter.start()
})A test that accepts done and also returns a promise fails because Mocha sees 2 completion signals.
Start and stop integration fixtures hooks-and-fixtures
describe('orders api', function () {
let server, db
before(async function () {
db = await startDatabase()
server = await startServer(db)
})
beforeEach(async function () {
await db.truncate('orders')
})
after(async function () {
await server.close()
await db.close()
})
})A failed before hook skips its suite tests; after must close servers and databases so --exit is unnecessary.
Pin discovery and timeout in .mocharc config-file
// .mocharc.json
{
"spec": ["test/**/*.spec.js"],
"recursive": true,
"timeout": 10000,
"require": ["./test/setup.js"],
"forbid-only": false,
"reporter": "spec"
}Mocha selects one config source, then CLI values override it; it does not merge every .mocharc format it finds.
Give a slow export 2 retries timeouts-and-retries
describe('report export', function () {
this.timeout(30000)
it('renders the quarterly PDF', function () {
this.retries(2)
return renderReport()
})
it('is fast', function () {
this.timeout(500)
})
})this.timeout() and this.retries() require function callbacks; the 2 retries apply to the test body, not a broken setup hook.
Fail CI when .only is committed focus-and-skip
describe.only('billing', function () {
it.skip('handles refunds', function () {})
it('charges the card', function () {})
})
# in CI, make a stray .only fail the build
mocha --forbid-only--forbid-only turns a committed focus marker into a failure; parallel mode rejects .only independently.
Share ESM database hooks esm-and-setup-file
// package.json
{ "type": "module" }
// test/setup.js
export const mochaHooks = {
async beforeAll() {
globalThis.db = await startDatabase()
},
async afterAll() {
await globalThis.db.close()
},
}
# .mocharc.json
{ "require": ["./test/setup.js"] }A module loaded through require can export mochaHooks; root-hook plugins are the documented shared lifecycle mechanism for parallel runs.
Run .ts specs through tsx typescript-tests
npm install --save-dev mocha tsx @types/mocha
// .mocharc.json
{
"extension": ["ts"],
"spec": ["test/**/*.spec.ts"],
"require": ["tsx"]
}extension finds .ts files and tsx executes them; tsc --noEmit remains separate because the loader is not the type-checking gate.
Run 4 test-file workers parallel-mode
mocha --parallel --jobs 4
// test/setup.js, required in .mocharc.json
export const mochaHooks = {
beforeEach() { /* runs in every worker */ },
}
export const mochaGlobalSetup = async () => { /* runs once, main process */ }Four worker processes do not share module state or file order, and parallel mode conflicts with --file, --sort, and .only.
Rerun specs after source changes watch-mode
mocha --watch --watch-files 'src/**/*.js,test/**/*.js' --reporter dotWatch mode keeps one process alive, so mutable module state can make a rerun differ from a clean mocha invocation.
Enforce 80 percent line coverage coverage-and-ci
npm install --save-dev c8
// package.json
{
"scripts": {
"test": "mocha",
"test:ci": "c8 --check-coverage --lines 80 mocha --forbid-only --fail-zero --reporter spec"
}
}c8 supplies coverage; --fail-zero stops a missing spec glob from passing after 0 tests execute.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| vitest | npm | Choose it for Vite-aware TypeScript transforms with integrated mocks, snapshots, and coverage |
| jest | npm | Choose it for built-in module mocking, snapshots, and established Babel or React Native support |
| ava | npm | Choose it for isolated processes, default concurrency, and its own assertion API |
| tap | npm | Choose it for TAP output, assertions, isolation, and coverage in one toolchain |
More testing guides
pytest · chai · vitest · jsdom · playwright · coverage · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

