JavaScript/TypeScript unit testing with Jest and Vitest as co-primary frameworks - install, config (`jest.config.js` / `vite.config.ts` test block), mocking (`jest.fn`/`jest.mock`/`jest.spyOn`, `vi.fn`/`vi.mock`/`vi.spyOn`, `__mocks__/`, fake timers), coverage (Istanbul/babel vs v8 providers, `coverageThreshold` gating), watch mode, and CI (`jest --ci`, `vitest run`, JUnit XML). Includes framework choice (Vite project → Vitest, otherwise Jest; always match an existing convention), test-authoring conventions (framework detection from package.json + config files, ESM vs CJS, no fabricated exports), and references for Mocha maintenance, Jasmine/Karma-to-Jest migration via jest-codemods, and deep Jest/Vitest coverage analysis. Use for any JS/TS unit-test task: setting up a framework, writing or mocking tests, gating coverage, or wiring CI.
93
93%
Does it follow best practices?
Impact
—
Average score across 10 eval scenarios
Passed
No findings from the security scan
Companion reference for js-unit-tests. Consult when maintaining a legacy
Mocha codebase, or when a library/tooling project prefers a minimal pluggable
runner over Jest/Vitest's batteries-included model. For new projects, prefer
Jest or Vitest (see the Choosing section of SKILL.md).
Per mochajs.org:
Mocha is the original mainstream JS test runner. Distinguishing features:
describe/it, default) and TDD (suite/test).--parallel flag for multi-process runs.npm install --save-dev mocha
npm install --save-dev chai sinon nyc # typical peers// test/sum.test.js
const { expect } = require('chai');
const { sum } = require('../src/sum');
describe('sum', () => {
it('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).to.equal(3);
});
});Wire "test": "mocha" in package.json. Default test glob:
./test/*.{js,cjs,mjs} plus ./test/**/*.spec.js. Node's built-in
node:assert works too when zero extra deps matter.
.mocharc.json configurationConfig files: .mocharc.json, .mocharc.js, .mocharc.yaml, or a mocha
key in package.json:
{
"spec": ["test/**/*.spec.js"],
"recursive": true,
"require": ["ts-node/register", "./test/setup.js"],
"reporter": "spec",
"timeout": 5000,
"parallel": true,
"jobs": 4,
"ui": "bdd",
"extension": ["js", "ts"]
}Key options: parallel + jobs (multi-process, Mocha 8+); ui: 'bdd'
(default) vs 'tdd'; recursive (nested test dirs); require (preload
TS support / setup).
Per mocha, three approaches - callback (done), returned promise,
and async/await (preferred):
it('async/await', async () => {
const result = await doAsyncWork();
expect(result).to.equal(42);
});The async function MUST return (or await) - otherwise the promise's
rejection isn't surfaced to Mocha and tests pass-by-accident.
before / after (once per describe block) and beforeEach / afterEach
(per test) all accept async bodies. it.only / describe.only run
exclusively; it.skip / xit skip. Forbid committed .only via
eslint-plugin-mocha's mocha/no-exclusive-tests rule.
{
"extends": "@istanbuljs/nyc-config-typescript",
"all": true,
"check-coverage": true,
"branches": 80, "lines": 80, "functions": 80, "statements": 80,
"include": ["src/**/*.{js,ts}"],
"reporter": ["text", "lcov", "html"]
}Run nyc mocha (Istanbul instrumentation), or c8 mocha (Node's built-in
V8 coverage; faster, no instrumentation). check-coverage + thresholds make
the run fail below the floor.
Per parallel mode (Mocha 8+): mocha --parallel --jobs 4.
Tests must be independent - shared state across describe blocks breaks
parallel runs.
Root hooks stop working in parallel mode. "Each test file gets its own
instance of Mocha", so a root hook defined in file A "will not be present"
in file B (mocha-par). The serial-era pattern -
--file ./test/setup.js installing a top-level before - does not carry
over. Two supported replacements (root hook plugins):
// test/hooks.js - loaded with `mocha --require test/hooks.js`
export const mochaHooks = {
beforeEach() { /* runs in every worker, before every test */ },
};
// once per run, not per worker ([global fixtures][mocha-gf]):
export const mochaGlobalSetup = async () => { /* seed */ };
export const mochaGlobalTeardown = async () => { /* tear down */ };- 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 }mocha-junit-reporter emits JUnit XML for junit-xml-analysis
(qa-test-reporting).
| Anti-pattern | Why it fails | Fix |
|---|---|---|
Forget return / await on async test body | Rejection silently passes the test | Always return or await |
Commit .only accidentally | CI runs only one test | mocha/no-exclusive-tests lint rule |
--parallel with a shared root before | Hooks run inconsistently per-process | Root hook plugins / global fixtures |
| Mix BDD + TDD interfaces | Reader confusion | Pick one in .mocharc.json ui: |
Skip check-coverage in nyc | Coverage gates not enforced | Enable + set thresholds |
mocha-chai-jest-snapshot.