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
- Getting Started
- Test Structure
- Hooks
- Assertions
- Async Testing
- Configuration
- Reporters
- Browser Testing
- Mocking
- Advanced Features
- Plugins
- CI/CD Integration
- Best Practices
- Debugging
- Performance
- Troubleshooting
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¶
Popular 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.