NodeJSMocha & Chai

Mocha & Chai

Mocha is one of the oldest and most flexible JavaScript test runners. Unlike Jest, it's deliberately minimal: it provides the test structure (describe/it, hooks, async support) but leaves assertions and mocking to libraries you choose — typically Chai for assertions and Sinon for mocks. This "bring your own pieces" philosophy gives maximum control and is favoured by teams who want to compose their own stack. This page covers Mocha's runner, Chai's three assertion styles, and combining them with Sinon.

Setup

Bash
npm install --save-dev mocha chai sinon

package.json

JSON
{
  "scripts": {
    "test": "mocha",
    "test:watch": "mocha --watch"
  }
}

.mocharc.json (config)

Text
{
  "spec": "test/**/*.test.js",
  "timeout": 5000,
  "recursive": true
}
A Mocha test with Chai

JS
import { expect } from 'chai'
import { calculateTotal } from '../src/cart.js'

describe('calculateTotal', () => {
  it('sums item prices', () => {
    expect(calculateTotal([{ price: 10 }, { price: 5 }])).to.equal(15)
  })

  it('returns 0 for an empty cart', () => {
    expect(calculateTotal([])).to.equal(0)
  })

  it('throws for invalid input', () => {
    expect(() => calculateTotal(null)).to.throw(/must be an array/)
  })
})
Mocha provides the structure; Chai provides the assertions — they're separate libraries
Mocha gives you `describe`, `it`, `before`/`after`/`beforeEach`/`afterEach`, async support, and the runner CLI — but **no assertion function**. You pair it with an assertion library, almost always Chai. This separation is Mocha's defining trait: you assemble your testing stack from focused tools rather than adopting one monolith. The trade-off is more setup decisions; the benefit is you can swap any piece independently.
Chai's three assertion styles

JS
import { expect, assert, should } from 'chai'
should()  // extends Object.prototype

const result = { name: 'Ada', age: 36 }

// 1. expect (BDD, most popular):
expect(result).to.have.property('name', 'Ada')
expect(result.age).to.be.a('number').and.above(18)

// 2. should (BDD, reads like prose):
result.should.have.property('name')
result.age.should.be.above(18)

// 3. assert (classic TDD):
assert.equal(result.name, 'Ada')
assert.isAbove(result.age, 18)
Pick one Chai style and stay consistent — `expect` is the most common
Chai offers three interchangeable styles: `expect` (chainable BDD, the most popular), `should` (extends `Object.prototype` so you write `value.should...`), and `assert` (a classic function-based API similar to `node:assert`). They're functionally equivalent — choose one for your codebase and use it everywhere. `expect` is the safest default: it doesn't modify prototypes (unlike `should`) and reads naturally.
Hooks

JS
describe('UserService', () => {
  let service, db

  before(async () => { db = await connectTestDb() })   // once before all
  after(async () => { await db.close() })              // once after all
  beforeEach(async () => {                              // before each test
    await db.users.deleteAll()
    service = new UserService(db)
  })

  it('creates a user', async () => {
    const user = await service.create({ email: 'a@x.com' })
    expect(user.id).to.exist
  })
})
Async tests

JS
// async/await — return the promise (Mocha awaits it):
it('fetches a user', async () => {
  const user = await getUser(1)
  expect(user.name).to.equal('Ada')
})

// chai-as-promised for promise assertions:
import chai from 'chai'
import chaiAsPromised from 'chai-as-promised'
chai.use(chaiAsPromised)

it('rejects for a missing user', async () => {
  await expect(getUser(999)).to.be.rejectedWith('not found')
})
Return or await async work in Mocha — a forgotten return means the assertion never runs
Like Jest, Mocha needs you to `return` the promise or use `async/await` so it waits for async work to finish. If you write an `it()` callback that creates a promise but doesn't return or await it, Mocha marks the test passed before the assertion runs. Use `async` functions and `await` everything; if you genuinely need callbacks, use Mocha's `done` parameter and call it exactly once.
Sinon for mocks, stubs, and spies

JS
import sinon from 'sinon'
import { expect } from 'chai'

describe('OrderService', () => {
  it('sends a confirmation after creating an order', async () => {
    // Stub — replace a method with a controllable fake:
    const emailService = { send: sinon.stub().resolves({ id: 'm1' }) }
    const service = new OrderService(emailService)

    await service.placeOrder({ item: 'book' })

    // Sinon assertions:
    expect(emailService.send.calledOnce).to.be.true
    expect(emailService.send.firstCall.args[0]).to.equal('order confirmed')
  })

  afterEach(() => sinon.restore())   // restore all stubs/spies
})

Sinon tool

Use

sinon.spy()

Wrap a function, record calls, keep original behaviour

sinon.stub()

Replace a function with a fake you control

sinon.mock()

Stub + pre-set expectations verified at the end

sinon.useFakeTimers()

Control setTimeout, Date, etc.

Mocha/Chai vs Jest
Mocha = flexibility and composition; Jest = batteries included
Mocha + Chai + Sinon gives you a stack you assemble and control — swap Chai for another assertion lib, swap Sinon for the [node:test mocks](/nodejs/node-test-runner), run in any environment. [Jest](/nodejs/jest) bundles all of this and adds snapshots and parallel execution out of the box. Choose Mocha if you value composability and minimalism or already have a Mocha codebase; choose Jest for an all-in-one experience with less setup. Both are mature and production-proven.
Next
Test your HTTP endpoints directly: [API Testing with Supertest](/nodejs/supertest).