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
npm install --save-dev mocha chai sinon
package.json
{
"scripts": {
"test": "mocha",
"test:watch": "mocha --watch"
}
}.mocharc.json (config)
{
"spec": "test/**/*.test.js",
"timeout": 5000,
"recursive": true
}A Mocha test with Chai
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/)
})
})Chai's three assertion styles
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)Hooks
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
// 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')
})Sinon for mocks, stubs, and spies
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 |
|---|---|
| Wrap a function, record calls, keep original behaviour |
| Replace a function with a fake you control |
| Stub + pre-set expectations verified at the end |
| Control |