Mocha

TestingUnit TestingJavaScriptNode.jsMatureFlexible

GitHub Overview

mochajs/mocha

☕️ simple, flexible, fun javascript test framework for node.js & the browser

Stars22,803
Watchers394
Forks3,033
Created:March 7, 2011
Language:JavaScript
License:MIT License

Topics

bddbrowserjavascriptmochamochajsnodenodejstddtesttest-frameworktestingtesting-tools

Star History

mochajs/mocha Star History
Data as of: 7/20/2025, 02:18 AM

Testing Tool

Mocha

Overview

Mocha is a flexible and mature testing framework for JavaScript. Developed since 2012, it runs on both Node.js and browsers. It supports multiple interfaces including BDD (Behavior Driven Development), TDD (Test Driven Development), Exports, QUnit, and Require, allowing developers to choose their preferred testing style. With a rich plugin ecosystem and high extensibility, it's a versatile testing tool that can adapt to any project requirements.

Details

Mocha's greatest strength lies in its flexibility and extensibility. It doesn't include an assertion library, allowing you to freely choose from Chai, Should.js, Node.js built-in assert module, or any other preferred library. This design enables building optimal combinations according to project requirements.

For asynchronous testing, it supports Promises, async/await, and callbacks, allowing concise description of complex asynchronous operations. The hook system (before, after, beforeEach, afterEach) enables executing necessary operations before and after tests. It also provides dynamic suite and test generation, conditional skipping, and retry functionality.

The reporting feature comes with over 20 built-in reporters, outputting results in various formats including JSON, HTML, TAP, and JUnit XML. Integration with CI/CD pipelines is straightforward.

Pros and Cons

Pros

  • High Flexibility: Freedom to choose assertion libraries and reporters
  • Rich Plugin Ecosystem: Comprehensive ecosystem from years of development
  • Multiple Interfaces: Various testing styles like BDD, TDD, Exports
  • Async Support: Full support for Promises, async/await, and callbacks
  • Browser Support: Runs on both Node.js and browsers
  • Stability: Long-term track record and reliability

Cons

  • Configuration Complexity: Initial setup can be complex due to flexibility
  • Separate Assertion Library: No built-in assertion library included
  • Learning Curve: Understanding diverse options and features required
  • Performance: Execution speed may be slower compared to newer frameworks

Reference Pages

Code Examples

Hello World

// package.json
{
  "scripts": {
    "test": "mocha"
  },
  "devDependencies": {
    "mocha": "^10.0.0",
    "chai": "^4.3.0"
  }
}

// test/hello.test.js
const assert = require('assert')

describe('Hello World', function() {
  it('should return greeting message', function() {
    function greet(name) {
      return `Hello, ${name}!`
    }
    
    assert.strictEqual(greet('Mocha'), 'Hello, Mocha!')
  })
})

Basic Testing

const { expect } = require('chai')

describe('Calculator', function() {
  let calculator

  beforeEach(function() {
    calculator = {
      add: (a, b) => a + b,
      subtract: (a, b) => a - b,
      multiply: (a, b) => a * b,
      divide: (a, b) => {
        if (b === 0) throw new Error('Division by zero')
        return a / b
      }
    }
  })

  describe('#add()', function() {
    it('should add positive numbers', function() {
      expect(calculator.add(2, 3)).to.equal(5)
    })

    it('should handle negative numbers', function() {
      expect(calculator.add(-1, 1)).to.equal(0)
    })

    it('should handle floating point precision', function() {
      expect(calculator.add(0.1, 0.2)).to.be.closeTo(0.3, 0.001)
    })
  })

  describe('#divide()', function() {
    it('should divide numbers correctly', function() {
      expect(calculator.divide(10, 2)).to.equal(5)
    })

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

Mocking

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

describe('API Client', function() {
  let fetchStub

  beforeEach(function() {
    fetchStub = sinon.stub(global, 'fetch')
  })

  afterEach(function() {
    fetchStub.restore()
  })

  it('should make API calls correctly', async function() {
    const mockResponse = {
      json: sinon.stub().resolves({ data: 'test data' }),
      ok: true
    }
    fetchStub.resolves(mockResponse)

    const apiClient = {
      async getData() {
        const response = await fetch('/api/data')
        return response.json()
      }
    }

    const result = await apiClient.getData()

    expect(fetchStub).to.have.been.calledWith('/api/data')
    expect(result).to.deep.equal({ data: 'test data' })
  })

  it('should handle API errors', async function() {
    fetchStub.rejects(new Error('Network error'))

    const apiClient = {
      async getData() {
        const response = await fetch('/api/data')
        return response.json()
      }
    }

    try {
      await apiClient.getData()
      expect.fail('Should have thrown an error')
    } catch (error) {
      expect(error.message).to.equal('Network error')
    }
  })
})

Async Testing

const { expect } = require('chai')

describe('Async Operations', function() {
  // Using Promises
  it('should handle promises', function() {
    const asyncFunction = () => {
      return new Promise((resolve) => {
        setTimeout(() => resolve('async result'), 100)
      })
    }

    return asyncFunction().then(result => {
      expect(result).to.equal('async result')
    })
  })

  // Using async/await
  it('should handle async/await', async function() {
    const asyncFunction = () => {
      return new Promise((resolve) => {
        setTimeout(() => resolve('async result'), 100)
      })
    }

    const result = await asyncFunction()
    expect(result).to.equal('async result')
  })

  // Using callbacks
  it('should handle callbacks', function(done) {
    const asyncFunction = (callback) => {
      setTimeout(() => callback(null, 'callback result'), 100)
    }

    asyncFunction((error, result) => {
      try {
        expect(error).to.be.null
        expect(result).to.equal('callback result')
        done()
      } catch (err) {
        done(err)
      }
    })
  })

  // Setting timeout
  it('should handle slow operations', async function() {
    this.timeout(5000) // 5 second timeout

    const slowFunction = () => {
      return new Promise(resolve => {
        setTimeout(() => resolve('slow result'), 2000)
      })
    }

    const result = await slowFunction()
    expect(result).to.equal('slow result')
  })
})

Setup and Teardown

const { expect } = require('chai')

describe('Database Operations', function() {
  let database

  // Before and after entire suite
  before(async function() {
    console.log('Setting up test database...')
    database = await setupTestDatabase()
  })

  after(async function() {
    console.log('Cleaning up test database...')
    await cleanupTestDatabase(database)
  })

  // Before and after each test
  beforeEach(async function() {
    await database.clear()
    await database.seed([
      { id: 1, name: 'Test User 1' },
      { id: 2, name: 'Test User 2' }
    ])
  })

  afterEach(async function() {
    await database.clear()
  })

  describe('User Operations', function() {
    it('should create a new user', async function() {
      const user = await database.createUser({ name: 'New User' })
      expect(user.id).to.exist
      expect(user.name).to.equal('New User')
    })

    it('should find existing users', async function() {
      const users = await database.findUsers()
      expect(users).to.have.length(2)
      expect(users[0].name).to.equal('Test User 1')
    })

    it('should update user information', async function() {
      const updatedUser = await database.updateUser(1, { name: 'Updated User' })
      expect(updatedUser.name).to.equal('Updated User')
    })
  })
})

Advanced Features

const { expect } = require('chai')

describe('Advanced Features', function() {
  // Conditional testing
  it('should run only on Node.js', function() {
    if (typeof window !== 'undefined') {
      this.skip()
    }

    expect(process.version).to.match(/^v\d+/)
  })

  // Dynamic test generation
  const testCases = [
    { input: 'hello', expected: 'HELLO' },
    { input: 'world', expected: 'WORLD' },
    { input: '123', expected: '123' }
  ]

  testCases.forEach(({ input, expected }) => {
    it(`should convert "${input}" to uppercase`, function() {
      expect(input.toUpperCase()).to.equal(expected)
    })
  })

  // Retry functionality
  it('should retry failed tests', function() {
    this.retries(3) // Maximum 3 retries

    // Simulating flaky test
    if (Math.random() > 0.7) {
      expect(true).to.be.true
    } else {
      expect(false).to.be.true
    }
  })

  // Custom reporter usage
  it('should support custom reporting', function() {
    console.log('Custom test output')
    expect(true).to.be.true
  })

  // Global hooks (root-level)
  before(function() {
    console.log('Global setup')
  })

  // Nested describe
  describe('Nested Suite', function() {
    describe('Deep Nesting', function() {
      it('should support deep nesting', function() {
        expect('nested').to.equal('nested')
      })
    })
  })
})