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, afterEach for 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

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');
    });
});
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