Skip to content
Mocha and Chai: Complete JavaScript Testing Guide with BDD and TDD

Mocha and Chai: Complete JavaScript Testing Guide with BDD and TDD

DodaTech Updated Jun 20, 2026 7 min read

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, and expect
  • TDD-style testing with suite, test, and assert
  • 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 is the difference between Mocha and Jest?
Mocha is a flexible framework that lets you choose your assertion library (Chai), mocking (Sinon), and coverage (nyc). Jest is an all-in-one framework with built-in assertions, mocking, and coverage.
Can I use Chai with Jest?
Yes, but Jest has built-in expect. Using Chai with Jest adds unnecessary complexity. Use Chai with Mocha instead.
How do I run a single test in Mocha?
Use it.only('test name', ...) to run only that test, or --grep pattern: mocha --grep "should handle".
How do I retry failing tests in Mocha?
Use this.retries(2) inside a test or describe block to retry failing tests up to 2 times.
How do I generate code coverage with Mocha?
Use nyc (Istanbul): nyc mocha tests/. Configure nyc in package.json for thresholds and reporting.

What’s Next

TutorialWhat You’ll Learn
Sinon Mocking and StubbingMocking, stubs, and spies for JavaScript tests
Jasmine and Karma TestingJasmine syntax and Angular testing with Karma
API Testing with PostmanREST 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