Introduction
Mocha and Chai form one of the most popular testing combinations in the JavaScript ecosystem. Mocha provides a flexible testing framework with excellent async support, while Chai offers expressive assertion styles that make tests readable and maintainable. Together, they create a powerful testing solution for Node.js and browser-based applications. For React component testing, consider Jest and Testing Library as a modern alternative.
Why Mocha and Chai?
Mocha Advantages
- Flexibility: Works with any assertion library (Chai, Should.js, Expect.js)
- Async support: First-class support for Promises, async/await, and callbacks
- Rich reporting: Multiple built-in reporters and extensible reporter system
- Browser support: Runs both in Node.js and browsers
- Serial execution: Tests run serially, allowing precise async flow control
Chai Advantages
- Multiple assertion styles: BDD (expect/should) and TDD (assert)
- Readable syntax: Natural language assertions
- Extensible: Rich plugin ecosystem
- Error messages: Clear, helpful error messages
Installation and Setup
Basic Installation
npm install --save-dev mocha chai
Project Structure
project/
├── src/
│ └── calculator.js
├── test/
│ ├── unit/
│ │ └── calculator.test.js
│ └── integration/
│ └── api.test.js
├── package.json
└── .mocharc.json
Configuration (.mocharc.json)
{
"require": ["test/setup.js"],
"spec": ["test/**/*.test.js"],
"timeout": 5000,
"reporter": "spec",
"recursive": true,
"exit": true
}
Package.json Scripts
{
"scripts": {
"test": "mocha",
"test:watch": "mocha --watch",
"test:coverage": "nyc mocha",
"test:unit": "mocha test/unit/**/*.test.js",
"test:integration": "mocha test/integration/**/*.test.js"
}
}
Chai Assertion Styles
1. Expect (BDD Style)
const { expect } = require('chai');
describe('Expect Style', () => {
it('should demonstrate expect assertions', () => {
const name = 'John';
const age = 30;
const hobbies = ['reading', 'coding'];
const user = { name: 'John', active: true };
// Equality
expect(name).to.equal('John');
expect(age).to.be.a('number');
// Deep equality for objects/arrays
expect(user).to.deep.equal({ name: 'John', active: true });
// Length and inclusion
expect(hobbies).to.have.lengthOf(2);
expect(hobbies).to.include('coding');
// Property existence
expect(user).to.have.property('name');
expect(user).to.have.property('name', 'John');
// Boolean checks
expect(user.active).to.be.true;
expect(undefined).to.be.undefined;
expect(null).to.be.null;
// Negation
expect(name).to.not.equal('Jane');
});
});
2. Should (BDD Style)
const chai = require('chai');
chai.should();
describe('Should Style', () => {
it('should demonstrate should assertions', () => {
const name = 'John';
const hobbies = ['reading', 'coding'];
name.should.equal('John');
name.should.be.a('string');
hobbies.should.have.lengthOf(2);
hobbies.should.include('coding');
});
});
3. Assert (TDD Style)
const { assert } = require('chai');
describe('Assert Style', () => {
it('should demonstrate assert assertions', () => {
const name = 'John';
const user = { name: 'John', age: 30 };
assert.equal(name, 'John');
assert.typeOf(name, 'string');
assert.lengthOf(name, 4);
assert.property(user, 'name');
assert.propertyVal(user, 'name', 'John');
assert.deepEqual(user, { name: 'John', age: 30 });
});
});
Real-World Testing Examples
Testing a Calculator Module
src/calculator.js:
class Calculator {
add(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new TypeError('Arguments must be numbers');
}
return a + b;
}
divide(a, b) {
if (b === 0) {
throw new Error('Division by zero');
}
return a / b;
}
async asyncMultiply(a, b) {
return new Promise((resolve) => {
setTimeout(() => resolve(a * b), 100);
});
}
}
module.exports = Calculator;
test/unit/calculator.test.js:
const { expect } = require('chai');
const Calculator = require('../../src/calculator');
describe('Calculator', () => {
let calculator;
beforeEach(() => {
calculator = new Calculator();
});
describe('#add()', () => {
it('should add two positive numbers', () => {
const result = calculator.add(5, 3);
expect(result).to.equal(8);
});
it('should add negative numbers', () => {
expect(calculator.add(-5, -3)).to.equal(-8);
});
it('should throw TypeError for non-number arguments', () => {
expect(() => calculator.add('5', 3)).to.throw(TypeError, 'Arguments must be numbers');
});
});
describe('#divide()', () => {
it('should divide two numbers', () => {
expect(calculator.divide(10, 2)).to.equal(5);
});
it('should throw error when dividing by zero', () => {
expect(() => calculator.divide(10, 0)).to.throw(Error, 'Division by zero');
});
});
describe('#asyncMultiply()', () => {
it('should multiply two numbers asynchronously', async () => {
const result = await calculator.asyncMultiply(4, 5);
expect(result).to.equal(20);
});
});
});
Testing API Service
For comprehensive API testing strategies, see our API testing mastery guide.
src/userService.js:
const axios = require('axios');
class UserService {
constructor(baseURL) {
this.client = axios.create({ baseURL });
}
async getUser(id) {
const response = await this.client.get(`/users/${id}`);
return response.data;
}
async createUser(userData) {
const response = await this.client.post('/users', userData);
return response.data;
}
}
module.exports = UserService;
test/unit/userService.test.js:
const { expect } = require('chai');
const sinon = require('sinon');
const UserService = require('../../src/userService');
const axios = require('axios');
describe('UserService', () => {
let userService;
let axiosStub;
beforeEach(() => {
userService = new UserService('https://api.example.com');
axiosStub = sinon.stub(axios, 'create').returns({
get: sinon.stub(),
post: sinon.stub()
});
});
afterEach(() => {
sinon.restore();
});
describe('#getUser()', () => {
it('should fetch user by id', async () => {
const mockUser = { id: 1, name: 'John' };
userService.client.get.resolves({ data: mockUser });
const user = await userService.getUser(1);
expect(user).to.deep.equal(mockUser);
expect(userService.client.get.calledWith('/users/1')).to.be.true;
});
it('should handle API errors', async () => {
userService.client.get.rejects(new Error('Network error'));
try {
await userService.getUser(1);
expect.fail('Should have thrown error');
} catch (error) {
expect(error.message).to.equal('Network error');
}
});
});
});
Mocha Hooks
Hook Types
describe('Hooks Example', () => {
before(() => {
// Runs once before all tests in this block
console.log('Setup: before all tests');
});
after(() => {
// Runs once after all tests in this block
console.log('Teardown: after all tests');
});
beforeEach(() => {
// Runs before each test in this block
console.log('Setup: before each test');
});
afterEach(() => {
// Runs after each test in this block
console.log('Teardown: after each test');
});
it('test 1', () => {
expect(true).to.be.true;
});
it('test 2', () => {
expect(false).to.be.false;
});
});
Async Hooks
describe('Async Hooks', () => {
before(async () => {
// Setup database connection
await database.connect();
});
after(async () => {
// Close database connection
await database.disconnect();
});
beforeEach(async () => {
// Clear database before each test
await database.clear();
});
});
Async Testing Patterns
1. Async/Await (Recommended)
describe('Async/Await Pattern', () => {
it('should fetch user data', async () => {
const user = await fetchUser(123);
expect(user.name).to.equal('John');
});
it('should handle errors', async () => {
try {
await fetchUser(999);
expect.fail('Should have thrown');
} catch (error) {
expect(error.message).to.include('not found');
}
});
});
2. Promises
describe('Promise Pattern', () => {
it('should fetch user data', () => {
return fetchUser(123).then(user => {
expect(user.name).to.equal('John');
});
});
it('should handle errors', () => {
return fetchUser(999).catch(error => {
expect(error.message).to.include('not found');
});
});
});
3. Callbacks (Legacy)
describe('Callback Pattern', () => {
it('should fetch user data', (done) => {
fetchUser(123, (error, user) => {
if (error) return done(error);
expect(user.name).to.equal('John');
done();
});
});
});
Chai Plugins
chai-http (HTTP Testing)
npm install --save-dev chai-http
const chai = require('chai');
const chaiHttp = require('chai-http');
const app = require('../src/app');
chai.use(chaiHttp);
const { expect } = chai;
describe('API Endpoints', () => {
it('GET /api/users should return users', (done) => {
chai.request(app)
.get('/api/users')
.end((err, res) => {
expect(res).to.have.status(200);
expect(res.body).to.be.an('array');
expect(res.body).to.have.lengthOf.at.least(1);
done();
});
});
it('POST /api/users should create user', async () => {
const res = await chai.request(app)
.post('/api/users')
.send({ name: 'John', email: 'john@example.com' });
expect(res).to.have.status(201);
expect(res.body).to.have.property('id');
expect(res.body.name).to.equal('John');
});
});
chai-as-promised (Promise Assertions)
npm install --save-dev chai-as-promised
const chai = require('chai');
const chaiAsPromised = require('chai-as-promised');
chai.use(chaiAsPromised);
const { expect } = chai;
describe('Promise Testing', () => {
it('should resolve with user data', () => {
return expect(fetchUser(123)).to.eventually.have.property('name', 'John');
});
it('should reject with error', () => {
return expect(fetchUser(999)).to.be.rejectedWith('User not found');
});
it('should fulfill', () => {
return expect(Promise.resolve('success')).to.be.fulfilled;
});
});
chai-spies (Spies and Stubs)
npm install --save-dev chai-spies
const chai = require('chai');
const spies = require('chai-spies');
chai.use(spies);
const { expect } = chai;
describe('Spy Testing', () => {
it('should call callback', () => {
const callback = chai.spy();
const processor = (fn) => fn();
processor(callback);
expect(callback).to.have.been.called();
expect(callback).to.have.been.called.once;
});
});
Mocha Reporters
Built-in Reporters
# Spec (default) - hierarchical view
mocha --reporter spec
# Dot matrix - minimal output
mocha --reporter dot
# JSON - machine-readable output
mocha --reporter json > results.json
# HTML - browser-friendly output
mocha --reporter html > results.html
# TAP - Test Anything Protocol
mocha --reporter tap
Custom Reporter Example
// custom-reporter.js
function CustomReporter(runner) {
const passes = [];
const failures = [];
runner.on('pass', (test) => {
passes.push(test);
});
runner.on('fail', (test, err) => {
failures.push({ test, err });
});
runner.on('end', () => {
console.log(`\n✓ ${passes.length} passing`);
console.log(`✗ ${failures.length} failing\n`);
});
}
module.exports = CustomReporter;
Usage: mocha --reporter ./custom-reporter.js
Code Coverage with NYC
Installation
npm install --save-dev nyc
Configuration (.nycrc.json)
{
"all": true,
"include": ["src/**/*.js"],
"exclude": ["test/**", "**/*.test.js"],
"reporter": ["html", "text", "lcov"],
"check-coverage": true,
"lines": 80,
"functions": 80,
"branches": 80,
"statements": 80
}
Running Coverage
nyc mocha
# With threshold enforcement
nyc --check-coverage --lines 90 mocha
Best Practices
1. Descriptive Test Names
// Bad
it('test 1', () => {});
// Good
it('should return 404 when user does not exist', () => {});
2. Arrange-Act-Assert Pattern
it('should calculate total price with discount', () => {
// Arrange
const cart = new ShoppingCart();
cart.addItem({ price: 100 });
const discount = 0.1;
// Act
const total = cart.calculateTotal(discount);
// Assert
expect(total).to.equal(90);
});
3. One Assertion Per Test
// Avoid multiple unrelated assertions
it('should validate user', () => {
expect(user.name).to.equal('John');
expect(user.age).to.equal(30);
expect(user.email).to.include('@');
});
// Better: separate tests
describe('User Validation', () => {
it('should have correct name', () => {
expect(user.name).to.equal('John');
});
it('should have correct age', () => {
expect(user.age).to.equal(30);
});
it('should have valid email', () => {
expect(user.email).to.include('@');
});
});
4. Avoid Test Interdependence
// Bad: tests depend on execution order
describe('Bad Example', () => {
let userId;
it('should create user', async () => {
const user = await createUser({ name: 'John' });
userId = user.id; // Shared state
});
it('should fetch user', async () => {
const user = await fetchUser(userId); // Depends on previous test
expect(user.name).to.equal('John');
});
});
// Good: independent tests
describe('Good Example', () => {
it('should create user', async () => {
const user = await createUser({ name: 'John' });
expect(user).to.have.property('id');
});
it('should fetch user', async () => {
const created = await createUser({ name: 'John' });
const fetched = await fetchUser(created.id);
expect(fetched.name).to.equal('John');
});
});
5. Use Test Fixtures
// test/fixtures/users.js
module.exports = {
validUser: {
name: 'John Doe',
email: 'john@example.com',
age: 30
},
invalidUser: {
name: '',
email: 'invalid-email'
}
};
// test/user.test.js
const fixtures = require('./fixtures/users');
describe('User Validation', () => {
it('should accept valid user', () => {
expect(validateUser(fixtures.validUser)).to.be.true;
});
});
CI/CD Integration
Integrate Mocha tests into your CI/CD pipeline following best practices from our CI/CD guide for testers.
GitHub Actions
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Generate coverage
run: npm run test:coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage/lcov.info
Conclusion
Mocha and Chai provide a powerful, flexible testing foundation for JavaScript projects. Mocha’s async-first design and flexible architecture combine perfectly with Chai’s expressive assertion styles to create readable, maintainable tests. By following best practices—descriptive test names, proper test isolation, and comprehensive coverage—you can build a robust test suite that catches bugs early and serves as living documentation for your codebase. For framework-specific testing, explore Jest for React or Cypress for E2E testing.
Start with simple unit tests, gradually add integration tests, leverage plugins for specialized testing needs, and integrate coverage reporting to ensure your application remains reliable and maintainable.