On this page8 sections
CommandsIntermediate6-8 min reference

Mocha & Chai

A practical reference for writing tests with Mocha (the runner) and Chai (the assertion library) — the classic JavaScript testing combo, still common in API tests, library code, and Cypress's underlying runner.

Test Structure

Mocha organises tests with describe() (suites) and it() (test cases). Both take a name and a callback.

describe("Login Page", function () {
  describe("with valid credentials", function () {
    it("redirects to dashboard", function () {
      // test body
    });
 
    it("displays a welcome message", function () {
      // test body
    });
  });
 
  describe("with invalid credentials", function () {
    it("shows an error message", function () {
      // test body
    });
  });
});

Aliases

FunctionAliasWhen
describecontextWhen the inner block describes a condition (e.g. context("when logged out"))
itspecifySame as it — choose what reads best

Focused / skipped

describe.only("just this suite", function () { /* ... */ });
it.only("just this test", function () { /* ... */ });
 
describe.skip("skip whole suite", function () { /* ... */ });
it.skip("skip this test", function () { /* ... */ });

.only is great while debugging — but a leftover .only in CI silently passes only the focused tests. Most teams ban it via lint rules (mocha/no-exclusive-tests).

Pending tests

it("should also handle X");      // no callback — marked pending

Hooks

describe("UserService", function () {
 
  before(function () {           // once before any test in this block
    // start the test DB, seed data
  });
 
  after(function () {            // once after all tests
    // stop the test DB
  });
 
  beforeEach(function () {       // before EVERY test, including nested
    // reset state
  });
 
  afterEach(function () {        // after EVERY test
    // clean up
  });
 
  it("creates a user", function () { /* ... */ });
});

Named hooks

Mocha shows the hook name on failure — useful when a hook causes the failure:

before("seed test database", async function () {
  await db.migrate();
  await db.seed();
});

Nested hooks

Outer beforeEach runs before inner beforeEach. Outer afterEach runs after inner afterEach. This lets you layer setup:

describe("Cart", function () {
  beforeEach(function () { this.cart = new Cart(); });
 
  describe("when empty", function () {
    it("total is 0", function () {
      expect(this.cart.total()).to.equal(0);
    });
  });
 
  describe("with items", function () {
    beforeEach(function () { this.cart.add({ price: 10 }); });
 
    it("total reflects the items", function () {
      expect(this.cart.total()).to.equal(10);
    });
  });
});

Root-level hooks

Hooks declared outside any describe apply to every test in the run. Use sparingly — they're easy to forget.

Chai Assertion Styles

Chai ships three interfaces. Pick one and stick with it.

// BDD — most popular
const { expect } = require("chai");
expect(value).to.equal(expected);
 
// BDD — adds .should onto Object.prototype (mutates global!)
const should = require("chai").should();
value.should.equal(expected);
 
// TDD — Node's `assert`-style
const { assert } = require("chai");
assert.equal(actual, expected);

Equality and truthiness

expect(result).to.equal(42);                    // ===
expect(result).to.not.equal(0);
expect(obj).to.deep.equal({ name: "QA" });      // structural compare
expect(obj).to.eql({ name: "QA" });             // alias for deep.equal
 
expect(value).to.be.true;
expect(value).to.be.false;
expect(value).to.be.null;
expect(value).to.be.undefined;
expect(value).to.exist;                         // not null and not undefined
expect(value).to.be.ok;                         // truthy

Type checking

expect("hello").to.be.a("string");
expect([1, 2]).to.be.an("array");
expect(42).to.be.a("number");
expect({}).to.be.an("object");
expect(() => {}).to.be.a("function");
expect(null).to.be.a("null");

Numeric comparison

expect(10).to.be.above(5);
expect(10).to.be.below(20);
expect(10).to.be.at.least(10);
expect(10).to.be.at.most(10);
expect(5).to.be.within(1, 10);
expect(0.1 + 0.2).to.be.closeTo(0.3, 0.0001);

Strings

expect("hello world").to.include("world");
expect("hello world").to.contain("world");      // alias
expect("hello").to.have.lengthOf(5);
expect("hello").to.have.length.above(3);
expect("test@example.com").to.match(/^.+@.+\..+$/);
expect("Hello").to.startWith("He");
expect("Hello").to.endWith("lo");

Arrays

expect([1, 2, 3]).to.include(2);
expect([1, 2, 3]).to.have.lengthOf(3);
expect([1, 2, 3]).to.have.members([3, 2, 1]);     // same items, any order
expect([1, 2, 3]).to.have.ordered.members([1, 2, 3]);
expect([1, 2, 3]).to.deep.include({ id: 1 });     // for arrays of objects
expect([1, 2, 3]).to.be.an("array").that.is.not.empty;

Objects

expect(obj).to.have.property("name");
expect(obj).to.have.property("name", "QA");
expect(obj).to.have.nested.property("user.address.city");
expect(obj).to.have.all.keys("id", "name", "email");
expect(obj).to.have.any.keys("name", "alias");
expect(obj).to.have.deep.property("data", { id: 1 });
expect(obj).to.deep.include({ name: "QA" });      // partial deep match

Errors

expect(() => badFn()).to.throw();
expect(() => badFn()).to.throw(Error);
expect(() => badFn()).to.throw("error message");
expect(() => badFn()).to.throw(TypeError, /invalid/);
expect(() => goodFn()).to.not.throw();

Async Testing

Three ways — pick the one that matches your code style.

async / await — the modern default

it("fetches a user", async function () {
  const user = await api.getUser(1);
  expect(user).to.have.property("name");
  expect(user.name).to.equal("Ada");
});

Returning a promise

it("fetches a user", function () {
  return api.getUser(1).then(user => {
    expect(user.name).to.equal("Ada");
  });
});

Mocha sees the returned promise and waits for it.

done callback (older code only)

it("loads with a callback", function (done) {
  fs.readFile("config.json", (err, data) => {
    if (err) return done(err);
    expect(JSON.parse(data)).to.have.property("apiUrl");
    done();
  });
});

Pick one per test. Mixing done with async/promise return causes Mocha to either time out or pass before the assertions finish.

Timeouts

it("slow integration test", async function () {
  this.timeout(15_000);              // 15 seconds for THIS test
  await someSlowOperation();
});
 
describe("integration", function () {
  this.timeout(30_000);              // applies to every test in this block
 
  it("...", async function () { /* ... */ });
});
 
// Disable timeout entirely (only when you really mean it)
this.timeout(0);

Mocha's default timeout is 2000ms. Don't crank it up to hide flakes — figure out the slowness first.

Asserting on rejected promises

With chai-as-promised:

const chai = require("chai");
const chaiAsPromised = require("chai-as-promised");
chai.use(chaiAsPromised);
const { expect } = chai;
 
it("rejects with a useful error", async function () {
  await expect(api.getUser(0)).to.eventually.be.rejectedWith("invalid id");
  await expect(api.getUser(1)).to.eventually.have.property("name", "Ada");
});

Configuration (.mocharc.yml)

A .mocharc.yml, .mocharc.cjs, or .mocharc.json at the project root configures Mocha for every run.

spec:
  - 'test/**/*.spec.ts'
require:
  - 'ts-node/register'
  - './test/setup.ts'
recursive: true
timeout: 10000
slow: 2000
reporter: 'mochawesome'
reporter-option:
  - 'reportDir=mochawesome-report'
  - 'reportFilename=index'
parallel: true
jobs: 4
retries: 2
bail: false
extension:
  - 'ts'
  - 'js'
OptionPurpose
specGlob(s) for test files
requireModules loaded before tests (TS register, global setup, env load)
recursiveSearch subdirectories under spec paths
timeoutPer-test timeout (ms); 0 disables
slowThreshold above which a test is reported as slow
reporterBuilt-in or installed reporter name
parallelRun files in parallel worker processes
jobsParallel worker count (defaults to CPU count − 1)
retriesAuto-retry failing tests N times
bailStop the whole run on the first failure
extensionFile extensions to consider as tests (with recursive)
forbid-onlyFail the run if any .only is left — recommended for CI

CLI flags override config: npx mocha --grep "login" --bail.

Reporters

Built-in

npx mocha --reporter spec        # default — hierarchical
npx mocha --reporter dot         # dot per test
npx mocha --reporter nyan        # the cat
npx mocha --reporter json        # machine output
npx mocha --reporter min         # only pass/fail line
npx mocha --reporter progress    # bar
npx mocha --reporter list        # one line per test

mochawesome — single-file HTML

npm install -D mochawesome
npx mocha --reporter mochawesome \
  --reporter-options reportDir=mocha-report,reportFilename=index

Produces mocha-report/index.html — self-contained, easy to attach as a CI artifact.

mocha-junit-reporter — JUnit XML for CI

npm install -D mocha-junit-reporter
npx mocha --reporter mocha-junit-reporter \
  --reporter-options mochaFile=test-results/junit.xml

GitHub Actions, Jenkins, GitLab — they all parse this format.

Multiple reporters at once — mocha-multi-reporters

npm install -D mocha-multi-reporters mocha-junit-reporter mochawesome

reporter-options.json:

{
  "reporterEnabled": "spec, mocha-junit-reporter, mochawesome",
  "mochaJunitReporterReporterOptions": { "mochaFile": "test-results/junit.xml" },
  "mochawesomeReporterOptions": { "reportDir": "mochawesome-report" }
}

Run:

npx mocha --reporter mocha-multi-reporters \
  --reporter-options configFile=reporter-options.json

Chai Plugins

Chai is small by default — plugins add the matchers you need.

PluginAdds
chai-httpHTTP assertions: chai.request(app).get('/users').end(...)
chai-as-promisedeventually / rejectedWith / fulfilled for promise assertions
chai-thingsArray element matchers: arr.should.contain.something.like({ … })
chai-subsetPartial object match: expect(obj).to.containSubset({ … })
chai-datetimecloseToTime, withinTime, ISO-string equality
sinon-chaiexpect(spy).to.have.been.calledOnceWith(arg)
const chai = require("chai");
chai.use(require("chai-as-promised"));
chai.use(require("sinon-chai"));
chai.use(require("chai-subset"));
 
expect(promise).to.eventually.equal("ok");
expect(actual).to.containSubset({ user: { id: 1 } });
expect(spy).to.have.been.calledWith("foo");

Mocha with Sinon (Mocking)

Sinon provides spies, stubs, mocks, and fake timers. It pairs naturally with Mocha — afterEach(() => sinon.restore()) is the cleanup pattern.

Spies — observe without changing behaviour

const sinon = require("sinon");
const sinonChai = require("sinon-chai");
chai.use(sinonChai);
 
it("logs each request", function () {
  const log = sinon.spy(console, "log");
  service.handle("hello");
  expect(log).to.have.been.calledOnce;
  expect(log).to.have.been.calledWith("[svc] hello");
  log.restore();
});

Stubs — replace behaviour

afterEach(() => sinon.restore());
 
it("calls the API with the right id", async function () {
  const stub = sinon.stub(api, "getUser").resolves({ id: 1, name: "Ada" });
 
  const user = await userService.load(1);
 
  expect(stub).to.have.been.calledOnceWith(1);
  expect(user.name).to.equal("Ada");
});
 
it("retries on transient failure", async function () {
  const stub = sinon
    .stub(api, "getUser")
    .onFirstCall().rejects(new Error("503"))
    .onSecondCall().resolves({ id: 1 });
 
  const user = await retryingClient.load(1);
  expect(stub).to.have.been.calledTwice;
  expect(user.id).to.equal(1);
});

Mocks — set expectations up front

it("calls save exactly once", function () {
  const mock = sinon.mock(repo);
  mock.expects("save").once().withArgs(sinon.match({ id: 42 }));
 
  service.persist({ id: 42, name: "Ada" });
 
  mock.verify();
});

Fake timers — control time

it("fires the callback after 1s", function () {
  const clock = sinon.useFakeTimers();
  const cb = sinon.spy();
 
  scheduleAfter(1000, cb);
 
  clock.tick(999);
  expect(cb).to.not.have.been.called;
  clock.tick(1);
  expect(cb).to.have.been.calledOnce;
 
  clock.restore();
});

useFakeTimers() mocks Date.now(), setTimeout, setInterval, requestAnimationFrame, process.nextTick — useful for testing scheduling logic without sleeping.

sinon.restore() discipline

Stubs and spies modify their target. If you forget to restore, later tests see the stubbed behaviour. Always pair with cleanup:

describe("UserService", function () {
  afterEach(() => sinon.restore());     // ← undoes every stub/spy/mock from the test
});