Mocha and Chai: Complete JavaScript Testing Guide with BDD and TDD
Mocha is a flexible JavaScript test framework that runs on Node.js and in the browser, while Chai is a powerful assertion library that pairs with Mocha to provide readable, expressive assertions in BDD or TDD style.
What You’ll Learn
- Setting up Mocha and Chai for a Node.js project
- BDD-style testing with
describe,it, andexpect - TDD-style testing with
suite,test, andassert - Using hooks:
before,after,beforeEach,afterEach - Testing asynchronous code with callbacks, promises, and async/await
Why Mocha and Chai Matter
Mocha is one of the most popular JavaScript test frameworks with over 30 million weekly npm downloads. Combined with Chai’s expressive assertions, it provides a flexible, unopinionated testing experience that works for any project — from small libraries to large enterprise applications. Unlike Jest which bundles everything, Mocha lets you choose your assertion library, mocking library, and reporting tools.
Doda Browser uses Mocha and Chai for testing its core JavaScript modules, choosing Sinon for mocking and nyc for coverage reporting.
Learning Path
flowchart LR
A[Testing Basics] --> B[Mocha & Chai<br/>You are here]
B --> C[Sinon Mocking]
C --> D[Test Automation CI/CD]
D --> E[Jasmine & Karma]
style B fill:#f90,color:#fff
Setup
npm install --save-dev mocha chai// package.json
{
"scripts": {
"test": "mocha 'tests/**/*.test.js'",
"test:watch": "mocha --watch 'tests/**/*.test.js'"
}
}BDD Style
BDD (Behavior-Driven Development) style uses natural language constructs:
const { expect } = require('chai');
// Calculator module to test
function add(a, b) { return a + b; }
function divide(a, b) {
if (b === 0) throw new Error('Division by zero');
return a / b;
}
describe('Calculator', () => {
describe('add()', () => {
it('should return the sum of two numbers', () => {
expect(add(2, 3)).to.equal(5);
});
it('should handle negative numbers', () => {
expect(add(-1, -1)).to.equal(-2);
});
it('should handle zero', () => {
expect(add(0, 5)).to.equal(5);
});
});
describe('divide()', () => {
it('should divide two numbers', () => {
expect(divide(10, 2)).to.equal(5);
});
it('should throw when dividing by zero', () => {
expect(() => divide(5, 0)).to.throw('Division by zero');
});
});
});Expected output:
Calculator
add()
✓ should return the sum of two numbers
✓ should handle negative numbers
✓ should handle zero
divide()
✓ should divide two numbers
✓ should throw when dividing by zero
5 passing (5ms)TDD Style
Mocha also supports TDD-style with suite, test, and assert:
const { assert } = require('chai');
suite('Calculator (TDD style)', () => {
suite('add()', () => {
test('should return the sum of two numbers', () => {
assert.equal(add(2, 3), 5);
});
test('should handle negative numbers', () => {
assert.equal(add(-1, -1), -2);
});
});
});Chai Assertion Styles
Chai offers three interfaces:
expect (BDD)
expect(foo).to.equal('bar');
expect(foo).to.be.a('string');
expect(arr).to.have.lengthOf(3);
expect(fn).to.throw(Error);
expect(obj).to.have.property('name').that.equals('Alice');should (BDD)
foo.should.equal('bar');
foo.should.be.a('string');
arr.should.have.lengthOf(3);assert (TDD)
assert.equal(foo, 'bar');
assert.typeOf(foo, 'string');
assert.lengthOf(arr, 3);
assert.throws(fn, Error);
assert.property(obj, 'name');Hooks
Hooks run setup and teardown code at specific points:
describe('User Database', () => {
let db;
// Run once before all tests
before(() => {
db = new Database('test');
});
// Run before each test
beforeEach(async () => {
await db.connect();
await db.seed();
});
// Run after each test
afterEach(async () => {
await db.cleanup();
});
// Run once after all tests
after(async () => {
await db.disconnect();
});
it('should find user by email', async () => {
const user = await db.findUser('alice@test.com');
expect(user.name).to.equal('Alice');
});
it('should return null for missing user', async () => {
const user = await db.findUser('missing@test.com');
expect(user).to.be.null;
});
});Hook Execution Order
before (once)
beforeEach
✓ test 1
afterEach
beforeEach
✓ test 2
afterEach
beforeEach
✓ test 3
afterEach
after (once)Async Testing
Mocha supports three patterns for async code:
Callbacks
it('should complete the request (callback)', (done) => {
fetchData((err, data) => {
if (err) return done(err);
expect(data).to.be.an('object');
done();
});
});Promises
it('should complete the request (promise)', () => {
return fetchData().then(data => {
expect(data).to.be.an('object');
});
});Async/Await
it('should complete the request (async/await)', async () => {
const data = await fetchData();
expect(data).to.be.an('object');
});Full Example: API Testing
const axios = require('axios');
const { expect } = require('chai');
describe('User API', () => {
const API = 'https://jsonplaceholder.typicode.com';
let createdUserId;
it('should fetch all users', async () => {
const response = await axios.get(`${API}/users`);
expect(response.status).to.equal(200);
expect(response.data).to.be.an('array');
expect(response.data.length).to.be.greaterThan(0);
});
it('should fetch a user by ID', async () => {
const response = await axios.get(`${API}/users/1`);
expect(response.data.id).to.equal(1);
expect(response.data).to.have.property('name');
expect(response.data).to.have.property('email');
});
it('should create a new user', async () => {
const response = await axios.post(`${API}/users`, {
name: 'Test User',
email: 'test@example.com',
});
expect(response.status).to.equal(201);
createdUserId = response.data.id;
});
it('should update a user', async () => {
const response = await axios.put(`${API}/users/${createdUserId}`, {
name: 'Updated User',
});
expect(response.data.name).to.equal('Updated User');
});
it('should return 404 for non-existent user', async () => {
try {
await axios.get(`${API}/users/99999`);
expect.fail('Should have thrown');
} catch (error) {
expect(error.response.status).to.equal(404);
}
});
});Common Mocha and Chai Mistakes
1. Forgetting to Return a Promise
Async tests that don’t return the promise or use done will pass immediately without waiting.
Fix: Always return the promise or use async/await.
2. Using Arrow Functions with this
Mocha’s this context provides test metadata and timeout settings. Arrow functions don’t bind this.
Fix: Use regular functions for test callbacks when accessing this.timeout() or this.retries().
3. Shared Mutable State
Tests that modify shared state interfere with each other.
Fix: Initialize state in beforeEach, not in describe or before.
4. Overly Complex Assertions
Nested expect chains are hard to debug when they fail.
Fix: One assertion per it block. Use descriptive test names.
5. Not Using Hooks for Cleanup
Leftover test data, open connections, and temporary files accumulate.
Fix: Always clean up in afterEach or after.
6. Hardcoding Timeouts
A test that takes 5 seconds in development might take 30 seconds in CI.
Fix: Set appropriate timeouts per test or globally, and increase for slow CI environments.
7. Testing Implementation Details
Testing private functions or internal state makes tests brittle.
Fix: Test only the public API of your modules.
Practice Questions
1. What are the three assertion styles provided by Chai?
expect (BDD), should (BDD), and assert (TDD).
2. How do you test asynchronous code in Mocha?
Using callbacks with done, returning promises, or using async/await.
3. What is the difference between before and beforeEach hooks?
before runs once before all tests. beforeEach runs before each individual test.
4. Why should you avoid arrow functions in Mocha test callbacks?
Arrow functions don’t bind this, so you can’t access this.timeout(), this.retries(), or other Mocha context methods.
5. What happens if you don’t return a promise in an async Mocha test?
The test passes immediately without waiting for the async operation to complete, potentially hiding failures.
Challenge: Create a test suite for a simple REST API using Mocha, Chai, and axios. Write tests for all CRUD operations, error handling, and edge cases. Use hooks to set up and tear down test data.
FAQ
What’s Next
| Tutorial | What You’ll Learn |
|---|---|
| Sinon Mocking and Stubbing | Mocking, stubs, and spies for JavaScript tests |
| Jasmine and Karma Testing | Jasmine syntax and Angular testing with Karma |
| API Testing with Postman | REST API testing and contract testing |
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro. Updated 2026-06-20.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro