Mocha test reporting
Mocha ships fifteen reporters and no memory. Every one of them describes the run in front of you and
then exits. Qualflare has a
native Mocha reporter
that turns those runs into hosted, historical reporting: AI clusters
failures by root cause, Mocha’s built-in retries become real per-attempt history instead of a count, and
every --parallel worker and CI shard lands in one launch.
Mocha’s reporters, explained
Mocha is deliberately unopinionated — it brings no assertion library and no opinion about output — so it compensates with reporters. Fifteen ship in the box:
mocha --reporter spec # the default: readable, and gone when the job ends
mocha --reporter json # machine-readable, one blob on stdout
mocha --reporter xunit # built-in XML, thinner than real JUnit
mocha --reporter dot # and: tap, min, list, progress, nyan, markdown,
# doc, json-stream, landing, github-actions spec. The default, and the one you actually read — a nested outline of yourdescribeblocks with timings. Terminal only.json. One machine-readable blob with pass/fail, duration and a retry count per test. The most detail Mocha gives you without a plugin — note that’s a retry count, not what each attempt did.xunit. Built-in XML. Thinner than what CI plugins usually expect from JUnit, which is why mocha-junit-reporter exists and is what most teams actually install.- mochawesome. The standard third-party HTML report — filterable, with tracebacks and optional screenshots. Genuinely good for inspecting one run; like any local file, it doesn’t survive to the next.
github-actions. Emits workflow annotations so failures appear inline on the PR diff. Scoped to one provider and one run, by design.
One structural quirk shapes every choice here: Mocha runs exactly one reporter. Jest and Vitest take an array; Mocha takes a string. Whichever you pick, you give up the others.
# Mocha runs exactly one reporter, so this replaces your spec output:
mocha --reporter @qualflare/mocha/reporter
# To keep both, mocha-multi-reporters is the long-standing answer
npm i -D mocha-multi-reporters Where Mocha’s reporters stop
All of them are per-run and local. Each run’s JSON, XML or HTML is independent and is overwritten by the
next one, so “has checkout completes an order been getting
flakier this month”, “which of these 40 failures share a root cause”, and “is this suite slower than it was
in March” are not questions any of them can answer.
The retry data is the sharpest example. Mocha is one of the few runners that retries out of the box, which
makes it one of the few that could tell you exactly what a flaky test did on each attempt — but the
json reporter records a number, and the number is the same
whether the first attempt timed out or threw. Answering the interesting question requires storing results
over time and analyzing them.
Send Mocha results to Qualflare
Point Mocha at the reporter in .mocharc.cjs:
// .mocharc.cjs
module.exports = {
reporter: '@qualflare/mocha/reporter',
// Array of `key=value`, NOT an object — see below.
reporterOption: ['environment=staging'],
// Only needed for the qualflare.*() metadata API.
require: ['@qualflare/mocha/hooks'],
}; Then run and upload. The reporter makes no network calls and holds no credential — it writes a directory, and the CLI uploads it:
npm install --save-dev @qualflare/mocha
npx mocha # writes ./qualflare-results, zero network calls
qf my-project collect ./qualflare-results # uploads it In CI that’s one added step (GitHub Actions shown — GitLab CI, Bitbucket Pipelines, and Jenkins work the same way). Authenticate the CLI once with your Qualflare access token, stored as a CI secret — see the CLI docs.
# .github/workflows/tests.yml
- name: Run tests
run: npx mocha
- name: Upload results to Qualflare
if: always() # upload even when tests fail — that's the point
run: qf my-project collect ./qualflare-results
Already producing XML with mocha-junit-reporter? That
works too — qf my-project collect results.xml --format junit.
You get status, duration and name; the native reporter is what adds per-attempt history, steps, attachments
and metadata.
What you get on top of Mocha
- Retries as history, not a count. Mocha’s
jsonreporter tells you a test was retried twice. The Qualflare reporter records what each attempt did — which error, which duration. - AI failure clustering. When one broken fixture or a downed dependency takes out 40 tests, Qualflare groups them by root cause — so you fix one thing instead of reading 40 stack traces.
- Flaky detection from history. Each test is scored from its pass/fail record across runs, which catches the intermittent tests that happened not to flake today — the ones in-run retries mask rather than expose.
- Parallel and sharded runs in one launch.
--parallelneeds no configuration; sharded CI jobs write into one directory thatqf collectreads once. - Steps, attachments and labels. Mocha has no notion of a step, a label or a linked issue. The reporter adds all of it.
- History, trends & defects. Pass rate, slowest tests, and flakiness over time across branches — plus a defect you can open straight from a failing run.
Mocha’s reporters vs Qualflare
| json / xunit / mochawesome | Qualflare | |
|---|---|---|
| Records that a test was retried | Yesa count | Yesper attempt |
| History across CI runs | — | Yes |
| Merges parallel workers & CI shards | — | Yes |
| AI failure clustering (root cause) | — | Yes |
| Flaky scoring over time | — | Yes |
| Steps, attachments & labels | — | Yes |
| Local, zero-setup, offline | Yes | — |
Complementary: keep spec for local runs, add Qualflare for
hosted, historical CI observability.
Get AI analysis on your Mocha runs
Start free — add the reporter, run qf collect, and get your first AI analysis in minutes.
Qualflare works the same with Jest, Vitest, Cypress, Playwright, pytest, Go, JUnit and 20+ more frameworks. Testing a mobile app too? See our Android (Espresso), iOS (XCTest), and Maestro guides. Weighing tools? See how it compares to other test management platforms, or browse all framework reporting guides. Every reporter is open source.
Three Mocha specifics worth knowing
First, and this one costs people real time:
reporterOption must be an
array, and getting it wrong fails silently. Mocha reads the key differently across versions, and in
no version does it complain:
// Works on Mocha 12 only. On 10 it is stringified into nothing usable;
// on 8 it is discarded outright — and the run stays green either way.
reporterOption: { environment: 'staging' } // ✗ silently ignored
// Works on every supported version.
reporterOption: ['environment=staging'] // ✓
On Mocha 10 an object is stringified into nothing usable; on Mocha 8 it is discarded before the reporter
sees it. Either way the run stays green and every option falls back to its default — so a mistyped
environment or output directory looks exactly like success. The Qualflare reporter detects the mangled
object on Mocha 10 and warns; on Mocha 8 nothing reaches it, so there is nothing to warn about. The
key=value array works everywhere.
Second, Mocha retries out of the box, which most runners don’t — Go and pytest have no retry concept at all without a plugin:
// Mocha is one of the few runners with retries built in
describe('checkout', function () {
this.retries(2); // or: mocha --retries 2
it('completes an order', async function () { /* ... */ });
});
Two caveats: retries apply to it blocks, not hooks, and
this.retries() needs a real
function rather than an arrow, since an arrow has no
this. Worth treating retries as a supplement to
history-based flaky scoring rather than a substitute — a retry only ever fires for a test that happened to
flake while you were watching.
Third, the metadata API needs a Root Hook Plugin. Mocha gives a
test body no way to identify itself — there is no equivalent of Jest’s
expect.getState() or Vitest’s
task.meta — so
--require @qualflare/mocha/hooks records which test is
running for these calls to attach to:
const { qualflare } = require('@qualflare/mocha');
it('checks out', async function () {
qualflare.label('feature', 'checkout');
qualflare.link('https://example.com/issue/42', { type: 'issue', name: 'QF-42' });
qualflare.tag('smoke');
await qualflare.step('add to cart', () => {
qualflare.parameter('sku', 'widget');
qualflare.parameter('token', process.env.TOKEN, { masked: true });
});
}); It’s optional. Without it every result, status, duration, error and retry is still reported in full; only the metadata is dropped, with a warning, rather than guessed at and attached to whichever test ran next. Full reference in the metadata API docs.
Frequently asked questions
How do I send Mocha results to Qualflare?
Install @qualflare/mocha and set reporter: "@qualflare/mocha/reporter" in .mocharc.cjs. Running npx mocha then writes a ./qualflare-results directory — the reporter makes no network calls — and qf my-project collect ./qualflare-results uploads it. If you already produce JUnit XML with mocha-junit-reporter, you can upload that instead with --format junit; you just get less detail.
Why does reporterOption have to be an array?
Because Mocha reads that key differently across versions, and the failure is silent. An object works only on Mocha 12; on Mocha 10 it is stringified into nothing usable, and on Mocha 8 it is discarded before the reporter sees it. In both cases the run stays green and every option falls back to its default, so a wrong environment or output directory looks like it worked. The key=value array form works on every supported version. On Mocha 10 the reporter detects the mangled object and warns; on Mocha 8 nothing reaches it, so there is nothing to warn about.
Can I keep my spec output and report to Qualflare at the same time?
Not with Mocha alone — unlike Vitest or Jest, Mocha runs exactly one reporter, so setting the Qualflare reporter replaces your spec output. mocha-multi-reporters is the long-standing workaround, though it has not shipped a release since 2020. In CI this usually does not matter, since the point of the run is the uploaded report rather than the scrollback; locally, most people keep a separate script without the reporter.
Does it work with mocha --parallel?
Yes, with no extra configuration. The reporter runs once in the main process and writes one report for the whole run, and the package’s own CI asserts that a run produces the same report with and without --parallel. For sharded CI, point every shard at the same output directory and collect once: each Mocha process writes a uniquely named file, so shards never overwrite each other.
Why does the metadata API need --require @qualflare/mocha/hooks?
Mocha gives a test body no way to identify itself — there is no equivalent of Jest’s expect.getState() or Vitest’s task.meta — so a Root Hook Plugin has to record which test is running for qualflare.label() and friends to attach to. It is optional: without it every result, status, duration, error and retry is still reported in full, and metadata calls are dropped with a warning rather than guessed at and attached to the wrong test.
Does Qualflare detect flaky Mocha tests?
Yes, from two sources. Mocha has retries built in, and the reporter records per-attempt history rather than just a retry count — so a test that failed twice and passed on the third attempt shows what each attempt did. On top of that, Qualflare scores each test from its pass/fail record across every run, which catches tests that are intermittent but happened not to flake today.
Setup reflects @qualflare/mocha and the Qualflare CLI (docs.qualflare.com) as of September 2026. Reporter names and flags were checked against Mocha 12. Written by İbrahim Süren, Qualflare.