Skip to content

Mocha Cheatsheet

Mocha - Simple, Flexible, Fun JavaScript Testing

Mocha is a feature-rich JavaScript test framework running on Node.js and in the browser, making asynchronous testing simple and fun. Mocha tests run serially, allowing for flexible and accurate reporting, while mapping uncaught exceptions to the correct test cases.

Table of Contents

Installation

Basic Installation

# Install Mocha globally
npm install -g mocha

# Install Mocha locally (recommended)
npm install --save-dev mocha

# Install with assertion library
npm install --save-dev mocha chai

# Install with additional tools
npm install --save-dev mocha chai sinon nyc

Project Setup

# Initialize new project
mkdir my-mocha-project
cd my-mocha-project
npm init -y

# Install dependencies
npm install --save-dev mocha chai

# Create test directory
mkdir test

# Create first test file
touch test/test.js

Package.json Configuration

{
  "scripts": {
    "test": "mocha",
    "test:watch": "mocha --watch",
    "test:coverage": "nyc mocha",
    "test:reporter": "mocha --reporter spec",
    "test:grep": "mocha --grep 'pattern'",
    "test:timeout": "mocha --timeout 5000"
  },
  "devDependencies": {
    "mocha": "^10.0.0",
    "chai": "^4.3.0",
    "sinon": "^15.0.0",
    "nyc": "^15.1.0"
  }
}

Directory Structure

my-project/
├── lib/
│   ├── calculator.js
│   └── utils.js
├── test/
│   ├── calculator.test.js
│   ├── utils.test.js
│   └── helpers/
│       └── setup.js
├── .mocharc.json
├── package.json
└── README.md

Getting Started

First Test

// lib/calculator.js
function add(a, b) {
  return a + b;
}

function subtract(a, b) {
  return a - b;
}

function multiply(a, b) {
  return a * b;
}

function divide(a, b) {
  if (b === 0) {
    throw new Error('Division by zero');
  }
  return a / b;
}

module.exports = { add, subtract, multiply, divide };
// test/calculator.test.js
const { expect } = require('chai');
const { add, subtract, multiply, divide } = require('../lib/calculator');

describe('Calculator', function() {
  describe('Addition', function() {
    it('should add two positive numbers', function() {
      expect(add(2, 3)).to.equal(5);
    });

    it('should add negative numbers', function() {
      expect(add(-2, -3)).to.equal(-5);
    });

    it('should add zero', function() {
      expect(add(5, 0)).to.equal(5);
    });
  });

  describe('Division', function() {
    it('should divide positive numbers', function() {
      expect(divide(10, 2)).to.equal(5);
    });

    it('should throw error for division by zero', function() {
      expect(() => divide(10, 0)).to.throw('Division by zero');
    });
  });
});

Running Tests

# Run all tests
npm test

# Run specific test file
npx mocha test/calculator.test.js

# Run tests with pattern
npx mocha test/**/*.test.js

# Run tests with grep
npx mocha --grep "Addition"

# Run tests in watch mode
npx mocha --watch

# Run tests with reporter
npx mocha --reporter json

Basic Test Syntax

// Test suite
describe('Feature Name', function() {
  // Test case
  it('should do something', function() {
    // Test implementation
  });

  // Pending test
  it('should do something else');

  // Skipped test
  it.skip('should skip this test', function() {
    // This won't run
  });

  // Only run this test
  it.only('should only run this test', function() {
    // Only this test will run
  });
});

Test Structure

Describe Blocks

describe('User Management', function() {
  describe('User Creation', function() {
    it('should create a new user', function() {
      // Test implementation
    });

    it('should validate user data', function() {
      // Test implementation
    });
  });

  describe('User Authentication', function() {
    it('should authenticate valid user', function() {
      // Test implementation
    });

    it('should reject invalid credentials', function() {
      // Test implementation
    });
  });
});

Nested Describes

describe('API', function() {
  describe('Users Endpoint', function() {
    describe('GET /users', function() {
      it('should return all users', function() {
        // Test implementation
      });

      it('should return 200 status', function() {
        // Test implementation
      });
    });

    describe('POST /users', function() {
      it('should create new user', function() {
        // Test implementation
      });

      it('should return 201 status', function() {
        // Test implementation
      });
    });
  });
});

Test Organization

// Good: Descriptive test names
describe('Email Validator', function() {
  it('should return true for valid email addresses', function() {
    // Test implementation
  });

  it('should return false for invalid email addresses', function() {
    // Test implementation
  });

  it('should handle edge cases like empty strings', function() {
    // Test implementation
  });
});

// Good: Group related functionality
describe('Shopping Cart', function() {
  describe('Adding Items', function() {
    it('should add item to cart');
    it('should update cart total');
    it('should increment item count');
  });

  describe('Removing Items', function() {
    it('should remove item from cart');
    it('should update cart total');
    it('should decrement item count');
  });
});

Test Context

describe('User Service', function() {
  context('when user exists', function() {
    it('should return user data', function() {
      // Test implementation
    });

    it('should update user successfully', function() {
      // Test implementation
    });
  });

  context('when user does not exist', function() {
    it('should return null', function() {
      // Test implementation
    });

    it('should throw error on update', function() {
      // Test implementation
    });
  });
});

Hooks

Basic Hooks

describe('Database Tests', function() {
  // Runs once before all tests in this describe block
  before(function() {
    console.log('Setting up database connection');
    // Setup code here
  });

  // Runs once after all tests in this describe block
  after(function() {
    console.log('Closing database connection');
    // Cleanup code here
  });

  // Runs before each test in this describe block
  beforeEach(function() {
    console.log('Preparing test data');
    // Setup for each test
  });

  // Runs after each test in this describe block
  afterEach(function() {
    console.log('Cleaning up test data');
    // Cleanup after each test
  });

  it('should save user', function() {
    // Test implementation
  });

  it('should find user', function() {
    // Test implementation
  });
});

Async Hooks

describe('Async Setup', function() {
  before(async function() {
    this.timeout(5000); // Increase timeout for setup

    // Async setup
    this.database = await connectToDatabase();
    this.server = await startServer();
  });

  after(async function() {
    // Async cleanup
    await this.database.close();
    await this.server.stop();
  });

  beforeEach(async function() {
    // Clear database before each test
    await this.database.clear();

    // Seed test data
    await this.database.seed({
      users: [
        { name: 'John', email: 'john@example.com' },
        { name: 'Jane', email: 'jane@example.com' }
      ]
    });
  });

  it('should find users', async function() {
    const users = await this.database.findAll('users');
    expect(users).to.have.length(2);
  });
});

Hook Inheritance

describe('Parent Suite', function() {
  before(function() {
    console.log('Parent before');
  });

  beforeEach(function() {
    console.log('Parent beforeEach');
  });

  describe('Child Suite', function() {
    before(function() {
      console.log('Child before');
    });

    beforeEach(function() {
      console.log('Child beforeEach');
    });

    it('should inherit hooks', function() {
      // Execution order:
      // 1. Parent before
      // 2. Child before
      // 3. Parent beforeEach
      // 4. Child beforeEach
      // 5. Test execution
    });
  });
});

Conditional Hooks

describe('Conditional Setup', function() {
  before(function() {
    if (process.env.NODE_ENV === 'test') {
      // Only run in test environment
      this.mockServer = startMockServer();
    }
  });

  beforeEach(function() {
    // Skip setup for specific tests
    if (this.currentTest.title.includes('integration')) {
      this.skip();
    }
  });

  it('should run unit test', function() {
    // This will run
  });

  it('should run integration test', function() {
    // This will be skipped by beforeEach
  });
});

Assertions

Chai Assertions

const { expect } = require('chai');

describe('Chai Assertions', function() {
  it('should test equality', function() {
    expect(2 + 2).to.equal(4);
    expect({ name: 'John' }).to.deep.equal({ name: 'John' });
    expect([1, 2, 3]).to.deep.equal([1, 2, 3]);
  });

  it('should test truthiness', function() {
    expect(true).to.be.true;
    expect(false).to.be.false;
    expect(null).to.be.null;
    expect(undefined).to.be.undefined;
    expect('hello').to.exist;
  });

  it('should test types', function() {
    expect('hello').to.be.a('string');
    expect(42).to.be.a('number');
    expect([]).to.be.an('array');
    expect({}).to.be.an('object');
    expect(() => {}).to.be.a('function');
  });

  it('should test properties', function() {
    const obj = { name: 'John', age: 30 };
    expect(obj).to.have.property('name');
    expect(obj).to.have.property('age', 30);
    expect(obj).to.have.all.keys('name', 'age');
  });

  it('should test arrays', function() {
    const arr = [1, 2, 3, 4, 5];
    expect(arr).to.have.length(5);
    expect(arr).to.include(3);
    expect(arr).to.include.members([2, 4]);
    expect(arr).to.have.ordered.members([1, 2, 3, 4, 5]);
  });

  it('should test strings', function() {
    expect('hello world').to.contain('world');
    expect('hello world').to.match(/^hello/);
    expect('hello world').to.have.length(11);
  });
});

Custom Assertions

const { expect } = require('chai');

// Extend Chai with custom assertion
chai.use(function(chai, utils) {
  chai.Assertion.addMethod('validEmail', function() {
    const obj = this._obj;
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

    this.assert(
      emailRegex.test(obj),
      'expected #{this} to be a valid email',
      'expected #{this} not to be a valid email'
    );
  });
});

describe('Custom Assertions', function() {
  it('should validate email addresses', function() {
    expect('user@example.com').to.be.validEmail;
    expect('invalid-email').to.not.be.validEmail;
  });
});

Should Style Assertions

const should = require('chai').should();

describe('Should Style', function() {
  it('should use should syntax', function() {
    const user = { name: 'John', age: 30 };

    user.should.be.an('object');
    user.should.have.property('name', 'John');
    user.age.should.equal(30);

    const numbers = [1, 2, 3];
    numbers.should.have.length(3);
    numbers.should.include(2);
  });
});

Assert Style Assertions

const assert = require('chai').assert;

describe('Assert Style', function() {
  it('should use assert syntax', function() {
    const user = { name: 'John', age: 30 };

    assert.isObject(user);
    assert.property(user, 'name');
    assert.equal(user.name, 'John');
    assert.equal(user.age, 30);

    const numbers = [1, 2, 3];
    assert.lengthOf(numbers, 3);
    assert.include(numbers, 2);
  });
});

Async Testing

Promises

describe('Promise Testing', function() {
  it('should resolve promise', function() {
    return fetchUser(1).then(user => {
      expect(user.name).to.equal('John');
    });
  });

  it('should reject promise', function() {
    return fetchUser(-1).catch(error => {
      expect(error.message).to.equal('User not found');
    });
  });

  it('should use async/await', async function() {
    const user = await fetchUser(1);
    expect(user.name).to.equal('John');
  });

  it('should handle async errors', async function() {
    try {
      await fetchUser(-1);
      throw new Error('Should have thrown');
    } catch (error) {
      expect(error.message).to.equal('User not found');
    }
  });
});

Callbacks

describe('Callback Testing', function() {
  it('should handle callbacks with done', function(done) {
    fetchUserCallback(1, (error, user) => {
      if (error) return done(error);

      try {
        expect(user.name).to.equal('John');
        done();
      } catch (assertionError) {
        done(assertionError);
      }
    });
  });

  it('should handle callback errors', function(done) {
    fetchUserCallback(-1, (error, user) => {
      try {
        expect(error).to.exist;
        expect(error.message).to.equal('User not found');
        expect(user).to.not.exist;
        done();
      } catch (assertionError) {
        done(assertionError);
      }
    });
  });
});

Timeouts

describe('Timeout Testing', function() {
  // Set timeout for entire suite
  this.timeout(5000);

  it('should complete within timeout', function(done) {
    // Set timeout for specific test
    this.timeout(2000);

    setTimeout(() => {
      expect(true).to.be.true;
      done();
    }, 1000);
  });

  it('should handle slow operations', async function() {
    this.timeout(10000);

    const result = await slowOperation();
    expect(result).to.exist;
  });

  it('should disable timeout', function(done) {
    this.timeout(0); // Disable timeout

    // Very slow operation
    setTimeout(done, 30000);
  });
});

Retries

describe('Retry Testing', function() {
  // Retry failed tests
  this.retries(3);

  it('should retry flaky test', function() {
    // This test might fail randomly
    if (Math.random() < 0.7) {
      throw new Error('Random failure');
    }
    expect(true).to.be.true;
  });

  it('should retry specific test', function() {
    this.retries(5);

    // Test implementation
  });
});

Configuration

Mocha Configuration File

// .mocharc.json
{
  "spec": "test/**/*.test.js",
  "require": ["test/helpers/setup.js"],
  "timeout": 5000,
  "reporter": "spec",
  "recursive": true,
  "exit": true,
  "bail": false,
  "grep": "",
  "invert": false,
  "checkLeaks": true,
  "globals": ["expect", "sinon"],
  "retries": 0,
  "slow": 75,
  "ui": "bdd"
}

JavaScript Configuration

// .mocharc.js
module.exports = {
  spec: 'test/**/*.test.js',
  require: ['test/helpers/setup.js'],
  timeout: 5000,
  reporter: 'spec',
  recursive: true,
  exit: true,

  // Environment-specific configuration
  ...(process.env.NODE_ENV === 'ci' && {
    reporter: 'json',
    timeout: 10000,
    retries: 2
  })
};

Setup File

// test/helpers/setup.js
const chai = require('chai');
const sinon = require('sinon');

// Global setup
global.expect = chai.expect;
global.sinon = sinon;

// Chai plugins
chai.use(require('chai-as-promised'));
chai.use(require('sinon-chai'));

// Global hooks
before(function() {
  console.log('Global setup');
});

after(function() {
  console.log('Global cleanup');
});

beforeEach(function() {
  // Reset sinon stubs/spies
  sinon.restore();
});

Environment Variables

// test/helpers/setup.js
process.env.NODE_ENV = 'test';
process.env.DATABASE_URL = 'mongodb://localhost:27017/test';
process.env.API_URL = 'http://localhost:3001';

// Load environment-specific config
if (process.env.CI) {
  // CI-specific setup
  process.env.TIMEOUT = '10000';
} else {
  // Local development setup
  process.env.TIMEOUT = '5000';
}

Multiple Configurations

// mocha.config.js
const baseConfig = {
  timeout: 5000,
  recursive: true,
  exit: true
};

const configs = {
  unit: {
    ...baseConfig,
    spec: 'test/unit/**/*.test.js',
    reporter: 'spec'
  },

  integration: {
    ...baseConfig,
    spec: 'test/integration/**/*.test.js',
    timeout: 10000,
    reporter: 'json'
  },

  e2e: {
    ...baseConfig,
    spec: 'test/e2e/**/*.test.js',
    timeout: 30000,
    reporter: 'tap'
  }
};

module.exports = configs[process.env.TEST_TYPE] || configs.unit;

Reporters

Built-in Reporters

# Spec reporter (default)
npx mocha --reporter spec

# JSON reporter
npx mocha --reporter json

# TAP reporter
npx mocha --reporter tap

# Dot reporter
npx mocha --reporter dot

# Progress reporter
npx mocha --reporter progress

# Min reporter
npx mocha --reporter min

# Landing reporter
npx mocha --reporter landing

# List reporter
npx mocha --reporter list

Custom Reporter

// reporters/custom-reporter.js
function CustomReporter(runner) {
  const stats = runner.stats;

  runner.on('start', function() {
    console.log('🚀 Starting test run...');
  });

  runner.on('suite', function(suite) {
    if (suite.root) return;
    console.log(`📁 ${suite.title}`);
  });

  runner.on('test', function(test) {
    console.log(`  ▶️  ${test.title}`);
  });

  runner.on('pass', function(test) {
    console.log(`  ✅ ${test.title} (${test.duration}ms)`);
  });

  runner.on('fail', function(test, err) {
    console.log(`  ❌ ${test.title}`);
    console.log(`     ${err.message}`);
  });

  runner.on('end', function() {
    console.log(`🏁 ${stats.passes} passed, ${stats.failures} failed`);
  });
}

module.exports = CustomReporter;

// Usage
// npx mocha --reporter ./reporters/custom-reporter.js

Reporter with File Output

// reporters/file-reporter.js
const fs = require('fs');
const path = require('path');

function FileReporter(runner, options) {
  const reportPath = options.reporterOptions?.output || 'test-results.json';
  const results = {
    stats: {},
    tests: [],
    failures: []
  };

  runner.on('start', function() {
    results.stats.start = new Date();
  });

  runner.on('pass', function(test) {
    results.tests.push({
      title: test.title,
      fullTitle: test.fullTitle(),
      duration: test.duration,
      state: 'passed'
    });
  });

  runner.on('fail', function(test, err) {
    results.tests.push({
      title: test.title,
      fullTitle: test.fullTitle(),
      duration: test.duration,
      state: 'failed',
      error: err.message
    });

    results.failures.push({
      title: test.fullTitle(),
      error: err.message,
      stack: err.stack
    });
  });

  runner.on('end', function() {
    results.stats.end = new Date();
    results.stats.duration = results.stats.end - results.stats.start;
    results.stats.passes = runner.stats.passes;
    results.stats.failures = runner.stats.failures;

    fs.writeFileSync(reportPath, JSON.stringify(results, null, 2));
    console.log(`Report written to ${reportPath}`);
  });
}

module.exports = FileReporter;

Multiple Reporters

# Install multi-reporter
npm install --save-dev mocha-multi-reporters

# Configuration file
# multi-reporter-config.json
{
  "reporterEnabled": "spec,json,tap",
  "jsonReporterOptions": {
    "output": "test-results.json"
  },
  "tapReporterOptions": {
    "output": "test-results.tap"
  }
}

# Run with multiple reporters
npx mocha --reporter mocha-multi-reporters --reporter-options configFile=multi-reporter-config.json

Browser Testing

Browser Setup

<!-- test/browser/index.html -->
<!DOCTYPE html>
<html>
<head>
  <title>Mocha Browser Tests</title>
  <link rel="stylesheet" href="https://unpkg.com/mocha/mocha.css">
</head>
<body>
  <div id="mocha"></div>

  <!-- Mocha -->
  <script src="https://unpkg.com/mocha/mocha.js"></script>

  <!-- Chai -->
  <script src="https://unpkg.com/chai/chai.js"></script>

  <!-- Setup -->
  <script>
    mocha.setup('bdd');
    const expect = chai.expect;
  </script>

  <!-- Your code -->
  <script src="../lib/calculator.js"></script>

  <!-- Your tests -->
  <script src="calculator.test.js"></script>

  <!-- Run tests -->
  <script>
    mocha.run();
  </script>
</body>
</html>

Browser Test File

// test/browser/calculator.test.js
describe('Calculator (Browser)', function() {
  it('should add numbers', function() {
    expect(add(2, 3)).to.equal(5);
  });

  it('should work with DOM', function() {
    const div = document.createElement('div');
    div.textContent = 'Hello World';
    document.body.appendChild(div);

    expect(div.textContent).to.equal('Hello World');

    document.body.removeChild(div);
  });

  it('should test localStorage', function() {
    localStorage.setItem('test', 'value');
    expect(localStorage.getItem('test')).to.equal('value');
    localStorage.removeItem('test');
  });
});

Webpack Integration

// webpack.config.js
const path = require('path');

module.exports = {
  mode: 'development',
  entry: './test/browser/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'test-bundle.js'
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env']
          }
        }
      }
    ]
  }
};

Puppeteer Integration

// test/browser/puppeteer.test.js
const puppeteer = require('puppeteer');
const { expect } = require('chai');

describe('Browser Tests with Puppeteer', function() {
  let browser, page;

  before(async function() {
    browser = await puppeteer.launch();
    page = await browser.newPage();
  });

  after(async function() {
    await browser.close();
  });

  it('should run tests in browser', async function() {
    await page.goto('file://' + __dirname + '/index.html');

    // Wait for tests to complete
    await page.waitForFunction(() => {
      return window.mochaResults && window.mochaResults.complete;
    });

    const results = await page.evaluate(() => window.mochaResults);
    expect(results.failures).to.equal(0);
  });
});

Mocking

Sinon Integration

const sinon = require('sinon');
const { expect } = require('chai');

describe('Mocking with Sinon', function() {
  afterEach(function() {
    sinon.restore();
  });

  it('should mock functions', function() {
    const callback = sinon.fake();
    const proxy = sinon.fake.returns(42);

    callback('hello', 'world');
    expect(callback.calledWith('hello', 'world')).to.be.true;

    expect(proxy()).to.equal(42);
  });

  it('should stub methods', function() {
    const user = {
      getName: () => 'John',
      setName: (name) => { this.name = name; }
    };

    const stub = sinon.stub(user, 'getName').returns('Jane');

    expect(user.getName()).to.equal('Jane');
    expect(stub.calledOnce).to.be.true;
  });

  it('should spy on methods', function() {
    const user = {
      getName: () => 'John'
    };

    const spy = sinon.spy(user, 'getName');

    user.getName();
    user.getName();

    expect(spy.calledTwice).to.be.true;
  });
});

HTTP Mocking

const nock = require('nock');
const axios = require('axios');
const { expect } = require('chai');

describe('HTTP Mocking', function() {
  afterEach(function() {
    nock.cleanAll();
  });

  it('should mock HTTP requests', async function() {
    nock('https://api.example.com')
      .get('/users/1')
      .reply(200, { id: 1, name: 'John' });

    const response = await axios.get('https://api.example.com/users/1');
    expect(response.data.name).to.equal('John');
  });

  it('should mock POST requests', async function() {
    nock('https://api.example.com')
      .post('/users', { name: 'Jane' })
      .reply(201, { id: 2, name: 'Jane' });

    const response = await axios.post('https://api.example.com/users', {
      name: 'Jane'
    });

    expect(response.status).to.equal(201);
    expect(response.data.id).to.equal(2);
  });

  it('should mock with delays', async function() {
    nock('https://api.example.com')
      .get('/slow')
      .delay(1000)
      .reply(200, { message: 'slow response' });

    const start = Date.now();
    await axios.get('https://api.example.com/slow');
    const duration = Date.now() - start;

    expect(duration).to.be.at.least(1000);
  });
});

Module Mocking

const proxyquire = require('proxyquire');
const { expect } = require('chai');

describe('Module Mocking', function() {
  it('should mock required modules', function() {
    const dbMock = {
      findUser: sinon.stub().returns({ id: 1, name: 'John' })
    };

    const UserService = proxyquire('../lib/user-service', {
      './database': dbMock
    });

    const user = UserService.getUser(1);
    expect(user.name).to.equal('John');
    expect(dbMock.findUser.calledWith(1)).to.be.true;
  });

  it('should mock with multiple dependencies', function() {
    const mocks = {
      './database': {
        connect: sinon.stub(),
        query: sinon.stub().returns([])
      },
      './logger': {
        log: sinon.stub(),
        error: sinon.stub()
      }
    };

    const Service = proxyquire('../lib/service', mocks);

    Service.initialize();
    expect(mocks['./database'].connect.called).to.be.true;
    expect(mocks['./logger'].log.called).to.be.true;
  });
});

Advanced Features

Parallel Testing

# Install parallel testing
npm install --save-dev mocha-parallel-tests

# Run tests in parallel
npx mocha-parallel-tests

# Specify max parallel processes
npx mocha-parallel-tests --max-parallel 4

Test Filtering

# Run tests matching pattern
npx mocha --grep "should add"

# Run tests NOT matching pattern
npx mocha --grep "should add" --invert

# Run tests in specific files
npx mocha test/unit/**/*.test.js

# Run tests with tags
npx mocha --grep "@slow"
// Tag tests with comments
describe('Calculator', function() {
  it('should add quickly @fast', function() {
    // Fast test
  });

  it('should handle complex calculations @slow', function() {
    // Slow test
  });
});

Dynamic Test Generation

describe('Dynamic Tests', function() {
  const testCases = [
    { input: [2, 3], expected: 5 },
    { input: [5, 7], expected: 12 },
    { input: [-1, 1], expected: 0 },
    { input: [0, 0], expected: 0 }
  ];

  testCases.forEach(({ input, expected }) => {
    it(`should add ${input[0]} + ${input[1]} = ${expected}`, function() {
      expect(add(input[0], input[1])).to.equal(expected);
    });
  });
});

// Async dynamic tests
describe('API Endpoints', function() {
  let endpoints;

  before(async function() {
    endpoints = await loadEndpointsConfig();
  });

  function createEndpointTest(endpoint) {
    it(`should respond to ${endpoint.method} ${endpoint.path}`, async function() {
      const response = await request(app)
        [endpoint.method.toLowerCase()](endpoint.path);

      expect(response.status).to.equal(endpoint.expectedStatus);
    });
  }

  // Generate tests after loading config
  before(function() {
    endpoints.forEach(createEndpointTest);
  });
});

Test Utilities

// test/helpers/utils.js
const { expect } = require('chai');

function expectAsync(promise) {
  return {
    toResolve: async () => {
      try {
        const result = await promise;
        return result;
      } catch (error) {
        throw new Error(`Expected promise to resolve, but it rejected with: ${error.message}`);
      }
    },

    toReject: async (expectedError) => {
      try {
        await promise;
        throw new Error('Expected promise to reject, but it resolved');
      } catch (error) {
        if (expectedError) {
          expect(error.message).to.include(expectedError);
        }
        return error;
      }
    }
  };
}

function createUser(overrides = {}) {
  return {
    id: Math.floor(Math.random() * 1000),
    name: 'Test User',
    email: 'test@example.com',
    ...overrides
  };
}

module.exports = { expectAsync, createUser };

Test Data Factories

// test/factories/user-factory.js
class UserFactory {
  static create(overrides = {}) {
    return {
      id: this.generateId(),
      name: 'John Doe',
      email: 'john@example.com',
      age: 30,
      active: true,
      createdAt: new Date(),
      ...overrides
    };
  }

  static createMany(count, overrides = {}) {
    return Array.from({ length: count }, (_, index) => 
      this.create({ id: index + 1, ...overrides })
    );
  }

  static generateId() {
    return Math.floor(Math.random() * 10000);
  }
}

module.exports = UserFactory;

// Usage in tests
const UserFactory = require('./factories/user-factory');

describe('User Service', function() {
  it('should process user', function() {
    const user = UserFactory.create({ name: 'Jane' });
    const result = processUser(user);
    expect(result.displayName).to.equal('Jane');
  });

  it('should handle multiple users', function() {
    const users = UserFactory.createMany(5);
    expect(users).to.have.length(5);
  });
});

Plugins

# Chai plugins
npm install --save-dev chai-as-promised chai-http chai-string

# Sinon integration
npm install --save-dev sinon-chai

# Coverage
npm install --save-dev nyc

# Parallel testing
npm install --save-dev mocha-parallel-tests

# Multi-reporters
npm install --save-dev mocha-multi-reporters

Chai Plugins Usage

const chai = require('chai');
const chaiAsPromised = require('chai-as-promised');
const chaiHttp = require('chai-http');
const chaiString = require('chai-string');

chai.use(chaiAsPromised);
chai.use(chaiHttp);
chai.use(chaiString);

const { expect } = chai;

describe('Chai Plugins', function() {
  it('should test promises', async function() {
    await expect(Promise.resolve('success')).to.eventually.equal('success');
    await expect(Promise.reject(new Error('fail'))).to.be.rejected;
  });

  it('should test HTTP', function() {
    return chai.request(app)
      .get('/api/users')
      .then(res => {
        expect(res).to.have.status(200);
        expect(res).to.be.json;
        expect(res.body).to.be.an('array');
      });
  });

  it('should test strings', function() {
    expect('Hello World').to.startWith('Hello');
    expect('Hello World').to.endWith('World');
    expect('Hello World').to.equalIgnoreCase('hello world');
  });
});

Custom Plugin Development

// chai-custom-plugin.js
module.exports = function(chai, utils) {
  chai.Assertion.addMethod('validEmail', function() {
    const obj = this._obj;
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

    this.assert(
      emailRegex.test(obj),
      'expected #{this} to be a valid email',
      'expected #{this} not to be a valid email'
    );
  });

  chai.Assertion.addMethod('between', function(min, max) {
    const obj = this._obj;

    this.assert(
      obj >= min && obj <= max,
      'expected #{this} to be between #{exp1} and #{exp2}',
      'expected #{this} not to be between #{exp1} and #{exp2}',
      `${min} and ${max}`
    );
  });
};

// Usage
const chai = require('chai');
chai.use(require('./chai-custom-plugin'));

describe('Custom Plugin', function() {
  it('should validate emails', function() {
    expect('user@example.com').to.be.validEmail;
  });

  it('should check ranges', function() {
    expect(5).to.be.between(1, 10);
  });
});

CI/CD Integration

GitHub Actions

# .github/workflows/test.yml
name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [16, 18, 20]

    steps:
    - uses: actions/checkout@v3

    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v3
      with:
        node-version: ${{ matrix.node-version }}
        cache: 'npm'

    - name: Install dependencies
      run: npm ci

    - name: Run tests
      run: npm test

    - name: Run coverage
      run: npm run test:coverage

    - name: Upload coverage to Codecov
      uses: codecov/codecov-action@v3
      with:
        file: ./coverage/lcov.info

Jenkins Pipeline

// Jenkinsfile
pipeline {
    agent any

    tools {
        nodejs '18'
    }

    stages {
        stage('Install') {
            steps {
                sh 'npm ci'
            }
        }

        stage('Test') {
            steps {
                sh 'npm test'
            }
            post {
                always {
                    publishTestResults testResultsPattern: 'test-results.xml'
                }
            }
        }

        stage('Coverage') {
            steps {
                sh 'npm run test:coverage'
            }
            post {
                always {
                    publishHTML([
                        allowMissing: false,
                        alwaysLinkToLastBuild: true,
                        keepAll: true,
                        reportDir: 'coverage',
                        reportFiles: 'index.html',
                        reportName: 'Coverage Report'
                    ])
                }
            }
        }
    }
}

Docker Integration

# Dockerfile.test
FROM node:18-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .

CMD ["npm", "test"]
# docker-compose.test.yml
version: '3.8'
services:
  test:
    build:
      context: .
      dockerfile: Dockerfile.test
    environment:
      - NODE_ENV=test
    volumes:
      - ./coverage:/app/coverage

Best Practices

Test Organization

// Good: Clear test structure
describe('UserService', function() {
  describe('#createUser', function() {
    context('with valid data', function() {
      it('should create user successfully', function() {
        // Test implementation
      });

      it('should return user with ID', function() {
        // Test implementation
      });
    });

    context('with invalid data', function() {
      it('should throw validation error', function() {
        // Test implementation
      });
    });
  });
});

// Good: Descriptive test names
it('should return 404 when user does not exist', function() {
  // Test implementation
});

it('should update user email when valid email provided', function() {
  // Test implementation
});

Test Data Management

// Good: Use factories for test data
const UserFactory = require('./factories/user-factory');

describe('User Tests', function() {
  it('should validate user', function() {
    const user = UserFactory.create({ email: 'invalid-email' });
    expect(() => validateUser(user)).to.throw();
  });
});

// Good: Clean up after tests
describe('Database Tests', function() {
  afterEach(async function() {
    await cleanupDatabase();
  });
});

Async Best Practices

// Good: Proper async handling
describe('Async Tests', function() {
  it('should handle promises', async function() {
    const result = await asyncOperation();
    expect(result).to.exist;
  });

  it('should handle errors', async function() {
    await expect(failingOperation()).to.be.rejected;
  });
});

// Good: Proper timeout handling
describe('Slow Tests', function() {
  this.timeout(10000);

  it('should complete slow operation', async function() {
    const result = await slowOperation();
    expect(result).to.exist;
  });
});

Debugging

Debug Mode

# Run with debug output
DEBUG=mocha* npm test

# Run specific test with debug
npx mocha --grep "specific test" --inspect-brk

# Debug with VS Code
# Add to launch.json
{
  "type": "node",
  "request": "launch",
  "name": "Mocha Debug",
  "program": "${workspaceFolder}/node_modules/.bin/mocha",
  "args": ["--timeout", "999999", "--colors", "${workspaceFolder}/test"],
  "console": "integratedTerminal",
  "internalConsoleOptions": "neverOpen"
}

Test Debugging

describe('Debug Tests', function() {
  it('should debug test', function() {
    const data = { name: 'John', age: 30 };

    console.log('Debug data:', data);
    debugger; // Breakpoint for debugging

    expect(data.name).to.equal('John');
  });

  it('should log test progress', function() {
    console.log('Step 1: Setup');
    const user = createUser();

    console.log('Step 2: Process');
    const result = processUser(user);

    console.log('Step 3: Verify');
    expect(result).to.exist;
  });
});

Error Debugging

describe('Error Debugging', function() {
  it('should provide detailed error info', function() {
    try {
      const result = complexOperation();
      expect(result.value).to.equal(42);
    } catch (error) {
      console.error('Test failed with error:', error);
      console.error('Stack trace:', error.stack);
      throw error;
    }
  });
});

Performance

Test Performance

// Optimize test setup
describe('Performance Tests', function() {
  // Use before/after for expensive setup
  before(async function() {
    this.database = await setupDatabase();
  });

  after(async function() {
    await this.database.close();
  });

  // Avoid unnecessary work in tests
  it('should be fast', function() {
    // Minimal test implementation
    expect(true).to.be.true;
  });
});

Parallel Execution

# Run tests in parallel
npx mocha-parallel-tests

# Specify parallel workers
npx mocha-parallel-tests --max-parallel 4

Memory Optimization

// .mocharc.js
module.exports = {
  // Optimize for memory usage
  timeout: 5000,
  bail: true, // Stop on first failure
  exit: true, // Force exit after tests

  // Cleanup after tests
  require: ['test/helpers/cleanup.js']
};

Troubleshooting

Common Issues

// Issue: Tests not running
// Solution: Check file patterns and paths
// .mocharc.json
{
  "spec": "test/**/*.test.js", // Correct pattern
  "recursive": true
}

// Issue: Async tests timing out
// Solution: Increase timeout or fix async handling
describe('Async Tests', function() {
  this.timeout(10000); // Increase timeout

  it('should handle async', async function() {
    const result = await asyncOperation();
    expect(result).to.exist;
  });
});

// Issue: Memory leaks
// Solution: Proper cleanup
afterEach(function() {
  // Clean up resources
  sinon.restore();
  nock.cleanAll();
});

Debug Configuration

// Debug configuration issues
console.log('Mocha config:', JSON.stringify(require('./.mocharc.json'), null, 2));

// Debug test discovery
npx mocha --dry-run

// Debug reporter issues
npx mocha --reporter json > test-results.json

Summary

Mocha is a flexible and powerful JavaScript testing framework that provides:

  • Flexible Structure: Support for BDD, TDD, and custom interfaces
  • Async Support: Native support for promises, callbacks, and async/await
  • Rich Ecosystem: Extensive plugin ecosystem and integrations
  • Multiple Environments: Runs in Node.js and browsers
  • Comprehensive Reporting: Multiple built-in reporters and custom reporter support
  • Advanced Features: Parallel testing, test filtering, and dynamic test generation
  • CI/CD Ready: Excellent integration with continuous integration systems
  • Debugging Support: Rich debugging capabilities and error reporting

Mocha excels at providing a solid foundation for JavaScript testing with its flexibility, extensive feature set, and mature ecosystem. Its unopinionated approach allows teams to build testing workflows that fit their specific needs while maintaining excellent developer experience and reliable test execution.