Skip to content

Early Adopter Offer:Get 40% off Core & Scale for your first year with code EARLYQFView pricing

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 your describe blocks 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 json reporter 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. --parallel needs no configuration; sharded CI jobs write into one directory that qf collect reads 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 retriedYesa countYesper attempt
History across CI runsYes
Merges parallel workers & CI shardsYes
AI failure clustering (root cause)Yes
Flaky scoring over timeYes
Steps, attachments & labelsYes
Local, zero-setup, offlineYes

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.

Get Started Free

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.