TL;DR
- Mocha provides test structure (
describe,it) — bring your own assertion library- Pair with Chai for readable assertions:
expect(value).to.equal(expected)- Hooks:
before,after,beforeEach,afterEachfor setup/teardown- Async support: callbacks (
done), promises, async/await all work- Flexible and modular — choose your tools, not locked into ecosystem
Best for: Node.js projects, teams wanting assertion library choice, legacy JS projects Skip if: You prefer all-in-one solutions (use Jest instead) Reading time: 14 minutes
Your Node.js project needs tests. Jest feels heavy for a small service. You want to choose your own assertion style. You need something that works with your existing tools.
Mocha stays out of your way. It runs tests, provides hooks, handles async — nothing more. You pick the assertion library, mocking tool, and reporter you want.
This tutorial covers Mocha from installation through advanced patterns — everything for solid JavaScript testing.
What is Mocha?
Mocha is a JavaScript test framework that runs on Node.js and in browsers. It provides structure for organizing tests but intentionally leaves assertion and mocking to other libraries.
Why Mocha:
- Flexible — works with any assertion library
- Async-friendly — callbacks, promises, async/await
- Rich ecosystem — many reporters and plugins
- Browser support — same tests run in Node and browser
- Mature — battle-tested since 2011
Mocha provides:
- Test organization (
describe,it) - Lifecycle hooks (
before,after, etc.) - Test runner with watch mode
- Reporter system
- Async handling
Mocha doesn’t provide:
- Assertions (use Chai, Node assert)
- Mocking (use Sinon)
- Coverage (use nyc/istanbul)
Installation and Setup
Basic Setup
npm install mocha chai --save-dev
// package.json
{
"scripts": {
"test": "mocha",
"test:watch": "mocha --watch"
}
}
Project Structure
project/
├── src/
│ └── calculator.js
├── test/
│ └── calculator.test.js
├── package.json
└── .mocharc.json
Configuration File
// .mocharc.json
{
"spec": "test/**/*.test.js",
"timeout": 5000,
"recursive": true,
"exit": true
}
Writing Your First Test
Basic Test Structure
// test/calculator.test.js
const { expect } = require('chai');
const Calculator = require('../src/calculator');
describe('Calculator', () => {
describe('add()', () => {
it('should add two positive numbers', () => {
const calc = new Calculator();
expect(calc.add(2, 3)).to.equal(5);
});
it('should handle negative numbers', () => {
const calc = new Calculator();
expect(calc.add(-1, 1)).to.equal(0);
});
});
describe('divide()', () => {
it('should divide two numbers', () => {
const calc = new Calculator();
expect(calc.divide(10, 2)).to.equal(5);
});
it('should throw on division by zero', () => {
const calc = new Calculator();
expect(() => calc.divide(10, 0)).to.throw('Division by zero');
});
});
});
Running Tests
# Run all tests
npm test
# Run specific file
npx mocha test/calculator.test.js
# Run with grep (filter by test name)
npx mocha --grep "add"
# Watch mode
npm run test:watch
Chai Assertions
Expect Style (Recommended)
const { expect } = require('chai');
// Equality
expect(value).to.equal(5);
expect(value).to.deep.equal({ a: 1 });
expect(value).to.not.equal(10);
// Type checking
expect('hello').to.be.a('string');
expect([1, 2]).to.be.an('array');
expect(null).to.be.null;
expect(undefined).to.be.undefined;
// Truthiness
expect(true).to.be.true;
expect(false).to.be.false;
expect(1).to.be.ok;
// Comparisons
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);
// Strings
expect('hello world').to.include('world');
expect('hello').to.have.lengthOf(5);
expect('hello').to.match(/^h/);
// Arrays
expect([1, 2, 3]).to.include(2);
expect([1, 2, 3]).to.have.lengthOf(3);
expect([1, 2, 3]).to.deep.include.members([2, 3]);
// Objects
expect({ a: 1 }).to.have.property('a');
expect({ a: 1, b: 2 }).to.include({ a: 1 });
expect({ a: 1 }).to.have.keys(['a']);
// Errors
expect(() => fn()).to.throw();
expect(() => fn()).to.throw(Error);
expect(() => fn()).to.throw('error message');
Should Style
require('chai').should();
value.should.equal(5);
'hello'.should.be.a('string');
[1, 2].should.have.lengthOf(2);
Assert Style
const { assert } = require('chai');
assert.equal(value, 5);
assert.isString('hello');
assert.lengthOf([1, 2], 2);
assert.throws(() => fn(), Error);
Lifecycle Hooks
Hook Types
describe('User Service', () => {
let db;
let userService;
before(async () => {
// Runs once before all tests in this describe
db = await connectToDatabase();
});
after(async () => {
// Runs once after all tests in this describe
await db.close();
});
beforeEach(() => {
// Runs before each test
userService = new UserService(db);
});
afterEach(async () => {
// Runs after each test
await db.collection('users').deleteMany({});
});
it('should create user', async () => {
const user = await userService.create({ name: 'John' });
expect(user.id).to.exist;
});
it('should find user by id', async () => {
const created = await userService.create({ name: 'Jane' });
const found = await userService.findById(created.id);
expect(found.name).to.equal('Jane');
});
});
Root Hooks
// test/hooks.js
exports.mochaHooks = {
beforeAll() {
// Runs once before all tests
},
afterAll() {
// Runs once after all tests
},
beforeEach() {
// Runs before each test
},
afterEach() {
// Runs after each test
}
};
// .mocharc.json
{
"require": ["test/hooks.js"]
}
Async Testing
Callbacks (done)
it('should fetch user', (done) => {
fetchUser(1, (err, user) => {
if (err) return done(err);
expect(user.name).to.equal('John');
done();
});
});
Promises
it('should fetch user', () => {
return fetchUser(1).then(user => {
expect(user.name).to.equal('John');
});
});
Async/Await (Recommended)
it('should fetch user', async () => {
const user = await fetchUser(1);
expect(user.name).to.equal('John');
});
it('should handle errors', async () => {
try {
await fetchUser(999);
expect.fail('Should have thrown');
} catch (err) {
expect(err.message).to.include('not found');
}
});
// Or with chai-as-promised
it('should reject with error', async () => {
await expect(fetchUser(999)).to.be.rejectedWith('not found');
});
Timeout Configuration
// Per-test timeout
it('should complete within 5 seconds', async function() {
this.timeout(5000);
await longRunningOperation();
});
// Per-suite timeout
describe('Slow tests', function() {
this.timeout(10000);
it('test 1', async () => { /* ... */ });
it('test 2', async () => { /* ... */ });
});
Mocking with Sinon
Installation
npm install sinon --save-dev
Spies
const sinon = require('sinon');
it('should call callback', () => {
const callback = sinon.spy();
doSomething(callback);
expect(callback.calledOnce).to.be.true;
expect(callback.calledWith('arg')).to.be.true;
});
Stubs
it('should use stubbed value', () => {
const user = { getName: () => 'Real Name' };
sinon.stub(user, 'getName').returns('Stubbed Name');
expect(user.getName()).to.equal('Stubbed Name');
user.getName.restore();
});
Mocking Modules
const sinon = require('sinon');
const axios = require('axios');
describe('API Client', () => {
let axiosStub;
beforeEach(() => {
axiosStub = sinon.stub(axios, 'get');
});
afterEach(() => {
axiosStub.restore();
});
it('should fetch data', async () => {
axiosStub.resolves({ data: { id: 1, name: 'Test' } });
const result = await apiClient.fetchUser(1);
expect(result.name).to.equal('Test');
expect(axiosStub.calledWith('/users/1')).to.be.true;
});
});
Test Organization
Nested Describes
describe('UserController', () => {
describe('POST /users', () => {
describe('with valid data', () => {
it('should return 201', () => { });
it('should create user', () => { });
});
describe('with invalid data', () => {
it('should return 400', () => { });
it('should return validation errors', () => { });
});
});
describe('GET /users/:id', () => {
it('should return user', () => { });
it('should return 404 for missing user', () => { });
});
});
Skip and Only
// Skip test
it.skip('should do something', () => { });
// Skip suite
describe.skip('Disabled tests', () => { });
// Run only this test
it.only('focus on this', () => { });
// Run only this suite
describe.only('Only these', () => { });
Reporters
Built-in Reporters
# Spec (default)
mocha --reporter spec
# Dot
mocha --reporter dot
# Min
mocha --reporter min
# JSON
mocha --reporter json > results.json
# List
mocha --reporter list
Mocha Awesome (HTML Report)
npm install mochawesome --save-dev
mocha --reporter mochawesome
CI/CD Integration
GitHub Actions
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm test
- name: Upload coverage
uses: codecov/codecov-action@v4
if: always()
Code Coverage with nyc
npm install nyc --save-dev
// package.json
{
"scripts": {
"test": "mocha",
"test:coverage": "nyc mocha"
}
}
Mocha with AI Assistance
AI tools can help write and maintain Mocha tests.
What AI does well:
- Generate test cases from function signatures
- Create assertion variations for edge cases
- Convert between assertion styles
- Suggest mock implementations
What still needs humans:
- Deciding what to test
- Understanding business logic
- Debugging flaky tests
- Performance considerations
FAQ
What is Mocha?
Mocha is a flexible JavaScript test framework for Node.js and browsers. It provides test structure (describe, it), lifecycle hooks (before, after), and async support, but intentionally doesn’t include assertions or mocking. You pair it with libraries like Chai for assertions and Sinon for mocking, giving you control over your testing toolkit.
Mocha vs Jest — which is better?
Jest is all-in-one with built-in assertions, mocking, and coverage. Mocha is modular — you choose each component. Jest is simpler to start with; Mocha offers more flexibility. Use Jest for React projects and quick setup. Use Mocha when you want specific assertion styles or have existing testing infrastructure.
What assertion library works with Mocha?
Chai is the most popular choice, offering three styles: expect (BDD), should (BDD), and assert (TDD). Node’s built-in assert module also works. Other options include should.js and expect.js. Most teams prefer Chai’s expect style for readability.
Can Mocha test async code?
Yes, excellently. Mocha supports three async patterns: callbacks (use done parameter), promises (return the promise), and async/await (just use async function). For promise-based assertions, add chai-as-promised. Mocha handles all patterns natively without configuration.
Official Resources
See Also
- Jest Testing Tutorial - All-in-one alternative
- Cypress Tutorial - E2E testing
- WebdriverIO Tutorial - Browser automation with Mocha
- GitHub Actions for QA - CI/CD integration
