Mocha is one of the most established testing frameworks in the JavaScript ecosystem.
Mocha takes a minimalist approach—offering a powerful test runner while allowing you to choose your assertion libraries, mocking tools, and test structure.
This flexibility has made Mocha a popular choice for testing in Node.js, where customization and modular tooling are often essential.
In this guide, you’ll learn how to use Mocha alongside Chai to write clear assertions and Sinon to create mocks and spies—equipping you with everything you need to build a reliable and maintainable testing workflow.
Prerequisites
You'll need Node.js installed on your system to work through this tutorial. While Mocha works with multiple Node.js versions, this guide uses features available in Node.js 22.x and above.
Step 1 — Setting up your testing environment
Mocha is designed for flexibility, with minimal setup required. In this section, you’ll set up a basic testing environment and prepare your project to use Mocha, along with other helpful testing tools.
Start by creating a new project directory:
mkdir mocha-testing && cd mocha-testing
Then, initialize a new Node.js project:
npm init -y
This creates a package.json
file to manage your project’s dependencies and settings.
Next, enable ES Modules in your project by adding a "type"
field to the package.json
:
npm pkg set type="module"
Now, install Mocha itself. While many frameworks handle everything from test discovery to assertions, Mocha primarily focuses on being an excellent test runner:
npm install --save-dev mocha
One of Mocha’s key design choices is to leave out an assertion library, allowing you to choose the style that fits your project best.
In this guide, you’ll use Chai, a widely used and versatile library that supports three assertion styles: should
, expect
, and assert
.
npm install --save-dev chai
Set up your test script in package.json
to run Mocha by adding the following under the "scripts"
section:
npm pkg set scripts.test="mocha"
Mocha follows convention over configuration principles, automatically looking for test files in a test
directory, with no need for complicated configuration files:
mkdir test
Next, create a sample module in the root directory to test. Because this project uses ES Modules, make sure to use export
and import
syntax:
export function multiply(a, b) {
return a * b;
}
This simplicity has helped Mocha remain one of the most widely used testing tools in the JavaScript ecosystem, consistently adapting to evolving development practices.
Step 2 — Writing your first test
In this step, you’ll write your first test using Mocha’s BDD-style syntax. The describe
and it
functions help organize tests in a readable, structured way.
Create a test file that uses Mocha’s nested structure and ES Module imports:
import { expect } from 'chai';
import { multiply } from '../utils.js';
describe('Utility Functions', function() {
describe('multiply function', function() {
it('should return 6 when multiplying 2 and 3', function() {
expect(multiply(2, 3)).to.equal(6);
});
});
});
This test shows how Mocha organizes tests using a clear, readable structure:
- The outer
describe
defines a test suite that groups related tests together. - The inner
describe
provides context for a specific function or feature. - The
it
block defines an individual test case with a clear expected outcome. - Chai’s
expect
style makes assertions easy to read and understand.
Mocha’s nested structure was a major shift when it was introduced. Unlike traditional xUnit-style frameworks, Mocha’s BDD-style syntax encourages writing tests in a way that reads like natural language. The it
function combines with the test description to form a complete sentence—like:
"it should return 6 when multiplying 2 and 3."
This approach helps bridge the gap between technical implementation and business logic, making test output more accessible.
Step 3 — Running your tests
In this section, you’ll run the test you wrote using Mocha’s built-in test runner. Mocha provides clear and structured output to help you quickly identify which tests pass or fail.
Run the test suite with the following command:
npm test
Mocha's default "spec" reporter presents results in a hierarchical tree that mirrors your test structure:
> mocha-testing@1.0.0 test
> mocha
Utility Functions
multiply function
✔ should return 6 when multiplying 2 and 3
1 passing (2ms)
The indentation and checkmarks provide immediate visual feedback about which tests passed. This human-friendly output is one of Mocha's most distinctive features, designed to make failures immediately apparent.
Now, expand your test coverage by adding more scenarios to ensure the multiply function behaves correctly under different conditions:
import { expect } from 'chai';
import { multiply } from '../utils.js';
describe('Utility Functions', function() {
describe('multiply function', function() {
it('should return 6 when multiplying 2 and 3', function() {
expect(multiply(2, 3)).to.equal(6);
});
it('should return 0 when multiplying by 0', function() {
expect(multiply(5, 0)).to.equal(0);
expect(multiply(0, 5)).to.equal(0);
});
it('should handle negative numbers correctly', function() {
expect(multiply(-2, 3)).to.equal(-6);
expect(multiply(2, -3)).to.equal(-6);
expect(multiply(-2, -3)).to.equal(6);
});
});
});
In this update, the highlighted test cases cover edge cases that are common sources of bugs:
- Multiplying by zero, which should always return
0
- Using negative numbers, which should follow standard multiplication rules for signs
These additional tests help ensure your function is reliable in various situations. As your test suite grows, covering typical and edge cases will help prevent regressions and catch issues early.
When you rerun the tests, Mocha's color-coded output makes scanning results effortless:
If a test fails, Mocha highlights the failure in red and shows exactly what went wrong, including the expected and actual values. This level of detail makes it easy to spot and fix issues quickly—no need to insert extra console.log
statements just to debug your tests.
Step 4 — Testing asynchronous code
Modern JavaScript applications frequently rely on asynchronous operations. Whether you're calling external APIs, reading files, or querying databases, handling async code in tests is essential. Mocha excels in this area, with built-in support for various async patterns.
Create an asynchronous function in the root directory to simulate an API call:
export function fetchUserData(userId) {
return new Promise((resolve, reject) => {
// Simulate API call delay
setTimeout(() => {
if (!userId) {
reject(new Error('User ID is required'));
return;
}
resolve({
id: userId,
name: 'User ' + userId,
role: 'Member'
});
}, 100);
});
}
This function returns user data after a simulated delay or throws an error if no user ID is provided. Now, test it using Mocha’s built-in support for async/await:
import { expect } from 'chai';
import { fetchUserData } from '../api.js';
describe('API Functions', function() {
// Increase timeout for all tests in this suite
this.timeout(500);
describe('fetchUserData function', function() {
it('should return user data for valid user ID', async function() {
const data = await fetchUserData(123);
expect(data).to.have.property('id', 123);
expect(data).to.have.property('name', 'User 123');
expect(data).to.have.property('role', 'Member');
});
it('should throw an error for missing user ID', async function() {
try {
await fetchUserData();
// If we reach here, the test should fail
expect.fail('Expected an error to be thrown');
} catch (error) {
expect(error.message).to.equal('User ID is required');
}
});
});
});
This test file shows how naturally Mocha supports asynchronous code using modern JavaScript:
- Declaring the test function as
async
lets Mocha wait for the Promise to resolve or reject. await
makes async code easier to read and reason about, especially in tests.- For success cases, results are awaited and directly asserted.
- A
try/catch
block ensures the expected error is properly handled and verified for failure cases.
The this.timeout(500)
line increases the timeout limit for the entire test suite. Mocha’s default timeout is 2000ms, but adjusting the timeout ensures the test doesn't fail prematurely in situations involving delayed responses—like simulated API calls.
When you run these tests with npm test
, you'll see results like:
API Functions
fetchUserData function
✔ should return user data for valid user ID (103ms)
✔ should throw an error for missing user ID (101ms)
Utility Functions
multiply function
✔ should return 6 when multiplying 2 and 3
...
5 passing (209ms)
The test output includes the execution time for each test, reflecting the simulated delay in your asynchronous function. Mocha automatically waits for any returned promises to resolve before moving on to the next test, ensuring that asynchronous logic is handled correctly without extra setup.
Step 5 — Using hooks for setup and teardown
When working with multiple tests, it's common to set up shared state before tests run and clean it up afterward. Mocha provides lifecycle hooks to handle these scenarios, helping you keep your test code organized and avoid repetition.
The following example shows how to use Mocha’s hooks by testing a simple counter class. Start by creating a file named counter.js
in the root directory:
export default class Counter {
constructor(initialValue = 0) {
this.count = initialValue;
}
increment() { return ++this.count; }
decrement() { return --this.count; }
getValue() { return this.count; }
}
Now, write tests using Mocha's hooks:
import { expect } from 'chai';
import Counter from '../counter.js';
describe('Counter Class', function() {
let counter;
beforeEach(function() {
counter = new Counter(); // Fresh instance for each test
});
it('should start at 0', function() {
expect(counter.getValue()).to.equal(0);
});
it('should increment correctly', function() {
counter.increment();
expect(counter.getValue()).to.equal(1);
});
});
This test file demonstrates the use of the beforeEach
hook, which runs before every individual test. In this case, it initializes a new Counter
instance before each test run. This ensures that tests are isolated and do not interfere with one another—an essential practice for writing reliable and maintainable test suites.
Run these tests with npm test
, and you'll see:
API Functions
...
Counter Class
✔ should start at 0
✔ should increment correctly
...
7 passing (210ms)
Mocha provides four main hooks that run in a specific sequence:
before(() => {/* runs once before all tests */});
beforeEach(() => {/* runs before each test */});
afterEach(() => {/* runs after each test */});
after(() => {/* runs once after all tests */});
- before: Runs once before any tests in the block begin. Ideal for one-time setup like database connections or loading test fixtures.
- beforeEach: Runs before each individual test. Perfect for creating fresh instances or resetting state, as shown in our example.
- afterEach: Runs after each individual test. Used for cleanup tasks like resetting mocks or clearing temporary data.
- after: Runs once after all tests complete. Suitable for closing connections or final cleanup.
These hooks are especially valuable when dealing with shared resources that require controlled setup and teardown—such as database connections, file system access, environment variable changes, or complex object initialization.
They’re also helpful for managing test doubles (like stubs and mocks), which you’ll explore in the next section. Using hooks properly ensures each test runs in a clean, isolated environment, leading to more consistent and trustworthy test results.
Step 6 — Mocking with Sinon.js
It's important to isolate those tests from the actual dependencies when testing code that relies on external systems—such as APIs, databases, or the file system.
This is where mocking becomes essential. Sinon.js is a powerful library that integrates smoothly with Mocha and provides test doubles like spies, stubs, and mocks.
Start by installing Sinon as a development dependency:
npm install --save-dev sinon
Next, create a service that depends on an external API client:
export default class UserService {
constructor(apiClient) {
this.apiClient = apiClient;
}
async getUserName(userId) {
const userData = await this.apiClient.fetchUserData(userId);
return userData.name;
}
}
This service method depends on an apiClient
to retrieve user information. The client might perform HTTP requests or database queries in a real-world scenario. However, in testing, you want to avoid relying on external systems.
Now, write a test using Sinon to create a test double for the apiClient
:
import { expect } from 'chai';
import sinon from 'sinon';
import UserService from '../userService.js';
describe('UserService', function() {
it('should return the user name from API data', async function() {
// Create a stub for the apiClient
const apiClientStub = {
fetchUserData: sinon.stub().resolves({
id: 123,
name: 'John Doe',
role: 'Member'
})
};
// Create a UserService with the stubbed apiClient
const userService = new UserService(apiClientStub);
// Call the method and verify the result
const name = await userService.getUserName(123);
expect(name).to.equal('John Doe');
// Verify the stub was called correctly
expect(apiClientStub.fetchUserData.calledOnce).to.be.true;
});
});
In this test, the apiClient
dependency is replaced with a stub that returns mock data. This allows the test to focus on the logic inside UserService
without relying on actual external calls.
This approach ensures your test is:
- Isolated from network or database dependencies
- Predictable, with controlled input and output
- Fast, since there are no real external operations
Run this test with npm test
, and you'll see:
UserService
✔ should return the user name from API data
...
8 passing (212ms)
Sinon makes it easy to test components in isolation through its flexible approach to test doubles.
Stubs allow you to replace real functions with controlled, predictable alternatives. Spies give you insight into how functions are used, such as how many times they're called and with which arguments, without changing their actual behavior.
Mocks go a step further, combining behavior replacement with built-in expectations about how functions should be used.
Using these techniques, you can simulate both normal and error conditions, keep tests fast and independent, and gain confidence that each piece of your code behaves as expected—even when working with external dependencies.
Final thoughts
Mocha’s minimalist, flexible design makes it a solid choice for testing JavaScript applications—especially in Node.js environments where control and customization are essential.
If you're looking for a more modern testing experience—especially in projects using Vite or working in the browser—consider Vitest. It offers a faster test runner, built-in mocking, snapshot support, and first-class TypeScript integration, all with a familiar syntax inspired by Mocha and Jest.
Make your mark
Join the writer's program
Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.
Write for us
Build on top of Better Stack
Write a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.
community@betterstack.comor submit a pull request and help us build better products for everyone.
See the full list of amazing projects on github