Testland
Browse all skills & agents

mocha-tests

Configures and runs Mocha - pluggable JS test runner pairable with Chai assertions, Sinon mocking, and nyc / c8 coverage; supports BDD interface (`describe` / `it`) and TDD interface (`suite` / `test`); async tests via callbacks / promises / async-await; `--parallel` mode (Mocha 8+); `.mocharc.json` config; per-test exclusivity via `.only()` / `.skip()`. Use when the user prefers a minimal pluggable runner (vs Jest's batteries-included) or maintains legacy Mocha codebases.

Install with skills.sh (any agent)

npx skills add testland/qa --skill mocha-tests
View source

mocha-tests

Overview

Per mochajs.org (opens in new window):

Mocha is the original mainstream JS test runner. Distinguishing features:

  • Pluggable: assertions (Chai / Node assert), mocking (Sinon / Jest's vi.fn equivalent), coverage (nyc / c8) are separate libraries - pick what you need.
  • Two interfaces: BDD (describe/it, default) and TDD (suite/test).
  • Reporter ecosystem: spec, json, html, tap, dot, nyan, mocha-junit-reporter, etc.
  • Parallel mode (Mocha 8+): --parallel flag for multi-process runs.

Modern web projects increasingly default to Jest or Vitest; Mocha remains strong for Node.js tooling, libraries, and pre-existing codebases.

When to use

  • Legacy codebase already on Mocha - migrating away has cost.
  • Library / tooling project where minimal-deps + pluggable architecture matters.
  • Team prefers Chai's assertion-fluency syntax over Jest's expect.
  • Need TDD-style interface (suite/test) over BDD.

For new browser-side projects, prefer vitest-tests or jest-tests.

Step 1 - Install

npm install --save-dev mocha
# Plus typical peers:
npm install --save-dev chai sinon nyc

Step 2 - First test

// test/sum.test.js
const assert = require('node:assert');
const { sum } = require('../src/sum');

describe('sum', () => {
  it('adds 1 + 2 to equal 3', () => {
    assert.strictEqual(sum(1, 2), 3);
  });
});

With Chai for fluent assertions:

const { expect } = require('chai');

describe('sum', () => {
  it('adds 1 + 2 to equal 3', () => {
    expect(sum(1, 2)).to.equal(3);
  });
});

Wire package.json:

{
  "scripts": {
    "test": "mocha"
  }
}

Default test glob: ./test/*.{js,cjs,mjs} plus ./test/**/*.spec.js.

Step 3 - .mocharc.json configuration

Mocha supports config files (.mocharc.json, .mocharc.js, .mocharc.yaml, or mocha key in package.json):

{
  "spec": ["test/**/*.spec.js"],
  "recursive": true,
  "require": ["ts-node/register", "./test/setup.js"],
  "reporter": "spec",
  "timeout": 5000,
  "slow": 1000,
  "parallel": true,
  "jobs": 4,
  "ui": "bdd",
  "diff": true,
  "extension": ["js", "ts"]
}

Key options:

  • parallel: true + jobs: N - multi-process parallel mode (Mocha 8+)
  • ui: 'bdd' (default) vs ui: 'tdd' - interface choice
  • recursive: true - scan nested test directories
  • require: [...] - preload modules (TS support, custom setup)

Step 4 - Async patterns

Per mocha (opens in new window), three async approaches:

// 1. Callback-based (legacy)
it('callback async', function(done) {
  setTimeout(() => {
    expect(true).to.be.true;
    done();
  }, 100);
});

// 2. Promise-based
it('promise async', () => {
  return doAsyncWork().then(result => {
    expect(result).to.equal(42);
  });
});

// 3. Async / await (modern; preferred)
it('async/await', async () => {
  const result = await doAsyncWork();
  expect(result).to.equal(42);
});

The async function MUST return (or await) - without that, the promise's rejection isn't surfaced to Mocha and tests pass-by-accident.

Step 5 - Hooks

describe('User service', () => {
  before(async () => {
    // runs once before all tests in this describe block
    await db.connect();
  });
  after(async () => {
    await db.disconnect();
  });
  beforeEach(() => {
    // runs before each test
  });
  afterEach(() => {
    // runs after each test
  });

  it('creates a user', async () => {
    // ...
  });
});

Hooks support async/promise patterns same as tests.

Step 6 - Exclusive + skipped tests

it.only('runs only this test', () => { /* ... */ });
describe.only('runs only tests in this block', () => { /* ... */ });
it.skip('skipped test', () => { /* ... */ });
xit('also skipped (alternate syntax)', () => { /* ... */ });

CI gating: forbid .only in committed code via lint rule (most codebases use eslint-plugin-mocha's mocha/no-exclusive-tests).

Step 7 - Coverage with nyc

npm install --save-dev nyc

.nycrc.json:

{
  "extends": "@istanbuljs/nyc-config-typescript",
  "all": true,
  "check-coverage": true,
  "branches": 80,
  "lines": 80,
  "functions": 80,
  "statements": 80,
  "include": ["src/**/*.{js,ts}"],
  "exclude": ["**/*.test.{js,ts}", "**/types.ts"],
  "reporter": ["text", "lcov", "html"]
}

Run:

nyc mocha

Modern alternative: c8 (uses Node's built-in V8 coverage; faster, no instrumentation):

npm install --save-dev c8
c8 mocha

Step 8 - Parallel mode

Per mocha (opens in new window) (Mocha 8+):

mocha --parallel --jobs 4

Tests must be independent - shared state across describe blocks breaks parallel runs. Limitations:

  • before / after hooks at the top-level scope don't run per-process consistently
  • File-level state mutations leak between describes in the same process

Step 9 - CI integration

- run: npm ci
- run: npx mocha --reporter mocha-junit-reporter --reporter-option mochaFile=./test-results/junit.xml
# Or with coverage:
- run: npx c8 --reporter lcov mocha
- uses: codecov/codecov-action@v4
  with: { files: ./coverage/lcov.info }

The mocha-junit-reporter package emits JUnit XML for junit-xml-analysis (in qa-test-reporting).

Anti-patterns

Anti-patternWhy it failsFix
Forget return / await on async test bodyPromise rejection silently passes the testAlways return or await (Step 4)
Commit .only accidentallyCI runs only one testLint rule mocha/no-exclusive-tests (Step 6)
Use --parallel with shared before hooksHooks run inconsistently per-processPer-test setup (Step 8)
Mix BDD + TDD interfacesReader confusionPick one in .mocharc.json ui: (Step 3)
Skip --check-coverage in nycCoverage gates not enforcedSet --check-coverage + thresholds (Step 7)

Limitations

  • Mocha doesn't bundle assertions / mocking / coverage - more setup vs Jest/Vitest.
  • Watch mode less polished than Vitest's.
  • Snapshot testing not built-in (use mocha-chai-jest-snapshot third-party).
  • ESM support workable but historically rough; pin a recent Mocha version.

References

  • mocha (opens in new window) - official site
  • mochajs.org/api - API reference
  • chaijs.com - Chai assertions (typical pairing)
  • sinonjs.org - Sinon mocking (typical pairing)
  • istanbul.js.org / github.com/bcoe/c8 - coverage tools
  • github.com/michaelleeallen/mocha-junit-reporter - JUnit XML reporter
  • jest-tests, vitest-tests, ava-tests, jasmine-tests - sister tools
  • test-code-conventions (qa-test-review) - test code hygiene

Related skills

ava-tests

Configures and runs AVA - concurrent-by-default JS/TS test framework with isolated test files (each file runs in its own Node process), no globals (explicit `import test from 'ava'`), async-first API, snapshot support, and TypeScript via `@ava/typescript`. Use when AVA is already the chosen framework and the user wants minimal-API parallel-by-default tests, works with libraries (vs apps) where per-file isolation prevents test interference, or is switching from Mocha for per-file process isolation Mocha cannot provide. For choosing between AVA and Mocha, or for Mocha-specific work, use mocha-tests.

jasmine-tests

Configures and runs Jasmine - the original BDD-style JS test framework (predecessor to Jest) with built-in matchers + spies, no external assertion library; ships `jasmine-core` + `jasmine` runner; `spec_dir` + `helpers` convention; `jasmine.json` config; spy patterns (`spyOn`, `createSpy`); pairs with Karma for in-browser testing. Use when the user maintains legacy AngularJS / Karma+Jasmine codebases, or wants minimal BDD-style tests with no third-party assertion library.

jest-tests

Configures and runs Jest - Meta-built batteries-included JS/TS unit framework with built-in `expect`, snapshot testing, mocking (`jest.mock`, `jest.fn`, `jest.spyOn`, manual `__mocks__/`), test environment selection (`jsdom` / `node`), parallel workers, coverage via Istanbul, watch mode, and CI integration via `--ci` flag. Use when the user works with React (CRA / older Next.js) or Node services and needs the most ecosystem-supported JS test framework.

vitest-tests

Configures and runs Vitest - Vite-native unit framework with Jest-compatible API (`expect`, `vi.fn`, `vi.mock`, `vi.spyOn`); reads `vite.config.*` so existing Vite plugins work; supports in-source testing via `if (import.meta.vitest)`, browser-mode UI for headed tests, type-checking via `vitest --typecheck`, native ESM, and coverage via v8 (default) or istanbul providers. Use when the user works with Vite-based projects (Vue, Svelte, Solid, modern React with Vite) or is migrating from Jest on an existing Vite project (not bundler-free Node - use jest-tests for that).