Mocha
GitHub Overview
mochajs/mocha
☕️ simple, flexible, fun javascript test framework for node.js & the browser
Topics
Star History
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')
})
})
})
})