Mocha is a long-standing JavaScript unit-test
runner. Mocha itself is not a benchmark framework, but its native
--reporter json option emits a single JSON document with per-test
duration fields (in milliseconds) — a perfectly usable performance
signal as long as test names are stable. See parser-targets.md
section 6 ("Unit test / QA frameworks").
This framework directory targets mocha's native JSON reporter, not
mocha-junit-reporter. A separate junit-producer parser can be added
later for the junit path if demand shows up; they are distinct output
formats and each gets its own parser per the resolved question in
docs/parser-targets.md.
- Sample benchmark — see
test/sample.test.js - Workflow —
.github/workflows/mocha.yml - Live run history — https://github.com/nyrkio/benchzoo/actions/workflows/mocha.yml
- Parser —
src/benchzoo/parsers/mocha.py(not yet written) - Parser tests —
tests/parsers/test_mocha.py(not yet written)
Implementation of the
canonical sample benchmark as four
Mocha it(...) tests inside a single describe('sample benchmark')
block in test/sample.test.js. Each it(...) title maps directly to
the test identifier the parser writes into attributes["test_name"]
(benchmark1 .. benchmark4).
- Test 1 (sleep 2.15 s) —
awaiton asetTimeout-backedsleep(2150)helper. Real timers; mocha has no fake-timer mode of its own, sosetTimeoutactually blocks wall-clock for ~2.15 s. - Test 2 (tight CPU loop) — sums
iinto asinkvariable across 1000 iterations. Touching the accumulator prevents a sufficiently clever JS engine from eliding the loop, analogous tostd::hint::black_boxin Rust orBlackhole.consumein JMH. - Test 3 (write 1.4 MB to /dev/null) — allocates a
Uint8Arrayof 1,400,000 bytes and fills it byte-by-byte. Node has no ergonomic/dev/nullwrite API; filling an in-process buffer is the closest equivalent and exercises the same I/O-shaped timing bucket. - Test 4 (monthly change point) — reads
new Date().getUTCMonth()(0-based → +1), computes2.15 + ((m % 3) - 1), and sleeps for that many seconds. Produces the step-function series described in the canonical sample benchmark.
this.timeout(10000) is set at the describe level so the
sleep-dominated tests (1 and 4, up to 3.15 s) never hit mocha's
default 2 s per-test timeout.
The workflow captures one output format: mocha's native
--reporter json, writing output.json in this directory. Mocha is
pinned (mocha@11.1.0) per
workflow-conventions.md.
The JSON reporter emits on stdout, not to a file, so run.sh
redirects:
npx mocha test/sample.test.js --reporter json --timeout 10000 > output.jsonact push -W .github/workflows/mocha.yml \
--artifact-server-path /tmp/benchzoo-artifactsCaptured output lands at
/tmp/benchzoo-artifacts/<run-id>/mocha-output/output.json. See
workflow-conventions.md
for the full act convention.
Alternatively, with Node 20 available locally you can skip act:
./run.shfrom this directory. That runs npm install && npx mocha ... > output.json
and produces the same output.json.
Mocha's native JSON reporter emits one JSON document per run, with the following top-level shape:
{
"stats": {
"suites": 1,
"tests": 4,
"passes": 4,
"pending": 0,
"failures": 0,
"start": "2026-04-13T12:00:00.000Z",
"end": "2026-04-13T12:00:08.500Z",
"duration": 8500
},
"tests": [ /* every test that ran, including passes, failures, pending */ ],
"pending": [ /* tests skipped with it.skip / xit */ ],
"failures": [ /* tests that threw or timed out */ ],
"passes": [ /* tests that succeeded */ ]
}Each entry inside tests / passes / failures / pending has:
| Field | Type | Notes |
|---|---|---|
title |
string | The it(...) title. For the sample: "benchmark1" .. "benchmark4". |
fullTitle |
string | describe path + title, space-joined. E.g. "sample benchmark benchmark1". |
file |
string | Absolute path to the test file. Not identity-bearing; workspace-dependent. |
duration |
number | Wall-clock duration in milliseconds. Integer for the test runner. |
currentRetry |
number | Retry count; 0 unless this.retries(n) is used in the test. |
err |
object | Empty {} on pass; populated with message/stack/name on failure. |
The same test appears in both tests and exactly one of
passes / failures / pending. The parser should iterate over
tests once (it's the union) and derive pass/fail from whether err
is an empty object or carries a message — or cross-reference against
failures[] by fullTitle. The passes / failures / pending
arrays are redundant views over the same data, useful for quick
summaries but not needed if the parser walks tests directly.
For the sample benchmark in this directory (no describe-nesting
beyond one level and no class-like grouping), the stable identity key
is title — "benchmark1" .. "benchmark4" — which maps directly to
attributes["test_name"]. If a future fixture nests multiple
describe blocks, fullTitle becomes the more obviously unique key,
but that is a fixture-drift concern to handle when it happens, not
pre-emptively.
duration is in milliseconds — convert to seconds (or carry as ms
with unit: "ms") when emitting the duration metric. The canonical
sample benchmark's ground truth is stated in seconds; the parser's
emitted metric should follow the same "seconds, lower_is_better"
convention the other unit-test-runner parsers (junit_standard,
junit_pytest) use, so downstream consumers can compare across
frameworks without unit juggling.
Failures should set passed: false on the result dict; the result is
still emitted, not filtered (see Library boundaries in
docs/design.md). A failed mocha test still
has a meaningful duration value — mocha records the wall time up to
the point of the assertion error or timeout — so the measurement stays
useful as a time series even when the test went red.
- Mocha timing precision.
durationis reported as an integer number of milliseconds. Test 2's tight CPU loop is expected to round to0or1ms — that is fine and expected; the value of test 2 in this framework is exercising the sub-millisecond floor, not producing a stable number. Parsers should not treat aduration: 0as a missing value. stats.start/stats.endare wall-clock strings, not commit timestamps. Per thetimestampfield semantics, the parser must leavetimestampat0and let the ingest layer assign the real git-commit-derived value. If the parser wants to preserve mocha's reported run time for reference, stash it inextra_info(e.g.extra_info["machine_time"]), never intimestamp.- Output file vs stdout. Mocha has a
--reporter-options output=...flag on some reporters, but the built-in JSON reporter ignores it and always writes to stdout.run.shredirects stdout intooutput.json. Don't be fooled by stale documentation suggesting the option works here.