Testing is an important part of building reliable Node.js applications.
When an application grows, manually checking every feature becomes difficult. A small change in one function can also break another part of the application.
This is where automated testing helps.
Mocha.js provides the framework for organizing and running your tests, while Chai provides expressive assertions for checking whether your code produces the expected results. Mocha does not require a specific assertion library, so Chai can be used alongside it.
In this complete guide, you’ll learn how to use Mocha.js and Chai together to test Node.js applications.
We’ll start with a simple test and gradually move into asynchronous code, errors, hooks, API-style functions, and testing best practices.
What Is Mocha.js?
Mocha.js is a JavaScript testing framework that can run tests in Node.js and the browser.
It provides features for:
- Organizing tests
- Running test cases
- Handling asynchronous code
- Running setup and cleanup hooks
- Reporting test results
- Working with different assertion libraries
Mocha is particularly useful for Node.js applications because asynchronous testing with callbacks, Promises, and async/await is built into the framework.
A simple Mocha test looks like this:
describe("Calculator", function() {
it("should add two numbers", function() {
// test code
});
});
Here:
describe()groups related tests.it()defines an individual test.
Mocha is responsible for running this test.
But we still need something to verify the result.
That’s where Chai comes in.
What Is Chai?
Chai is a JavaScript assertion library.
An assertion checks whether the actual result matches what you expect.
For example:
expect(10).to.equal(10);
This checks whether 10 is equal to 10.
Chai provides three main assertion styles:
expectshouldassert
The expect and should styles use a chainable BDD syntax, while assert provides a more traditional assertion style.
Mocha vs Chai
One of the first things beginners should understand is that Mocha and Chai have different responsibilities.
Mocha
Mocha is responsible for:
Organizing tests
↓
Running tests
↓
Handling async tests
↓
Reporting results
Chai
Chai is responsible for:
Receiving actual value
↓
Comparing it with expected value
↓
Passing or failing the assertion
For example:
it("should add two numbers", function() {
const result = add(10, 20);
expect(result).to.equal(30);
});
Mocha runs the test.
Chai checks the result.
Together, they provide a complete testing workflow.
Why Use Mocha.js and Chai Together?
You can use Mocha with Node.js’s built-in assert module.
However, Chai provides additional assertion styles and a readable chain-based syntax. Mocha’s documentation explicitly supports Chai and other assertion libraries.
Compare these two assertions.
Node.js assert
assert.strictEqual(result, 30);
Chai
expect(result).to.equal(30);
Both can verify the result.
Chai’s syntax can become particularly expressive when testing objects, arrays, properties, errors, and more complex values.
Setting Up Mocha and Chai
Let’s create a simple Node.js project.
Step 1: Create a Project
Run:
mkdir mocha-chai-demo
Move into the project:
cd mocha-chai-demo
Initialize npm:
npm init -y
Step 2: Install Mocha and Chai
Install both as development dependencies:
npm install --save-dev mocha chai
Mocha’s current documentation recommends installing it as a development dependency.
Step 3: Check Your Node.js Version
Current Mocha 12 requires:
Node.js ^20.19.0 || >=22.12.0
So check your version:
node -v
If you’re using an older Node.js version, upgrade Node.js before installing the current Mocha release.
Creating the Test Folder
Create a test folder:
mocha-chai-demo/
│
├── test/
│
├── package.json
└── package-lock.json
Mocha’s standard setup uses a test/ directory for test files.
Your First Mocha and Chai Test
Let’s create a simple calculator.
Create:
calculator.js
Add:
function add(a, b) {
return a + b;
}
module.exports = {
add
};
Now create:
test/calculator.test.js
Add:
const { expect } = require("chai");
const { add } = require("../calculator");
describe("Calculator", function() {
it("should add two numbers", function() {
const result = add(10, 20);
expect(result).to.equal(30);
});
});
Run the test:
npx mocha
Mocha will discover the test and execute it. The current Mocha getting-started documentation uses npx mocha as the basic way to run tests.
Adding a Test Script
Instead of typing:
npx mocha
every time, you can add a script to package.json.
{
"scripts": {
"test": "mocha"
}
}
Now run:
npm test
This is the standard test-script pattern shown in Mocha’s getting-started documentation.
Understanding describe() and it()
Let’s break down the test.
describe("Calculator", function() {
it("should add two numbers", function() {
const result = add(10, 20);
expect(result).to.equal(30);
});
});
describe()
describe() groups related tests.
describe("Calculator", function() {
});
You can put multiple tests inside it.
it()
it() represents one behavior you want to test.
it("should add two numbers", function() {
});
A good test description should explain what the function is expected to do.
Using Chai’s expect()
The expect style is one of the easiest Chai styles for beginners.
Import it:
const { expect } = require("chai");
Then:
expect(result).to.equal(30);
The syntax can be read almost like English:
Expect the result to equal 30.
Chai’s BDD API provides chainable language such as to, be, have, and not to make assertions readable.
Common Chai Assertions
Let’s look at the assertions you’ll use most often.
equal()
Use equal() when you want strict equality.
expect(10).to.equal(10);
It uses JavaScript’s strict equality behavior.
For example:
expect("10").to.not.equal(10);
not
You can negate an assertion:
expect(10).to.not.equal(20);
However, Chai recommends being precise about what you expect instead of using broad negative assertions whenever possible.
true and false
You can check exact boolean values:
expect(true).to.be.true;
expect(false).to.be.false;
These are preferable when you specifically expect a boolean value rather than merely checking whether something is truthy.
Checking Types
You can check the type of a value:
expect("John").to.be.a("string");
expect(25).to.be.a("number");
expect([]).to.be.an("array");
expect({}).to.be.an("object");
Testing Strings
Suppose we have:
const message = "Hello Node.js";
We can test its value:
expect(message).to.equal("Hello Node.js");
We can check whether it contains text:
expect(message).to.include("Node.js");
We can check its length:
expect(message).to.have.lengthOf(13);
These assertions make string-related tests easy to read.
Testing Numbers
Suppose:
const price = 500;
You can write:
expect(price).to.equal(500);
You can also test relationships:
expect(price).to.be.greaterThan(100);
expect(price).to.be.lessThan(1000);
However, if you know the exact expected result, prefer checking that exact result.
For example:
expect(price).to.equal(500);
is usually clearer than:
expect(price).to.be.within(400, 600);
when the expected value is specifically 500.
Testing Arrays
Suppose:
const users = ["John", "Mike", "Sarah"];
Check the type:
expect(users).to.be.an("array");
Check the length:
expect(users).to.have.lengthOf(3);
Check an item:
expect(users).to.include("John");
Check an item does not exist:
expect(users).to.not.include("David");
Testing Objects
Suppose we have:
const user = {
id: 1,
name: "John",
role: "admin"
};
Check the object:
expect(user).to.be.an("object");
Check a property:
expect(user).to.have.property("name");
Check the property and value:
expect(user).to.have.property("name", "John");
Chai supports property assertions with an expected value as part of its BDD assertion API.
Comparing Objects With deep.equal()
Consider:
const actual = {
name: "John",
age: 25
};
const expected = {
name: "John",
age: 25
};
This won’t work as a content comparison:
expect(actual).to.equal(expected);
JavaScript objects are reference values.
Use:
expect(actual).to.deep.equal(expected);
Now Chai compares the contents.
The same works with arrays:
const actual = [1, 2, 3];
const expected = [1, 2, 3];
expect(actual).to.deep.equal(expected);
Chai’s documentation specifically distinguishes strict equal() from deep equality using .deep.equal().
Testing Errors
Testing errors is an important part of a good test suite.
Consider this function:
function divide(a, b) {
if (b === 0) {
throw new Error("Cannot divide by zero");
}
return a / b;
}
We can test the error with Chai:
it("should throw an error when dividing by zero", function() {
expect(() => divide(10, 0))
.to.throw("Cannot divide by zero");
});
You can also check the error type:
expect(() => divide(10, 0))
.to.throw(Error);
Or both:
expect(() => divide(10, 0))
.to.throw(Error, "Cannot divide by zero");
Chai recommends checking the expected error type and message when those details are part of the behavior being tested.
An Important Mistake With throw()
Don’t do this:
expect(divide(10, 0)).to.throw();
The function executes before Chai can perform the assertion.
Instead, wrap the call:
expect(() => divide(10, 0)).to.throw();
This gives Chai a function that it can execute and inspect.
Testing Asynchronous Code
Node.js applications frequently use asynchronous operations.
For example:
function getUser() {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
id: 1,
name: "John"
});
}, 500);
});
}
Mocha supports asynchronous tests using:
- Callbacks with
done() - Promises
async/await
Mocha’s documentation recommends using one asynchronous completion mechanism rather than combining them.
Testing With async/await
For modern Node.js code, async/await makes asynchronous tests easy to read.
it("should return user information", async function() {
const user = await getUser();
expect(user).to.be.an("object");
expect(user).to.have.property("id", 1);
expect(user).to.have.property("name", "John");
});
Mocha waits for the Promise returned by the async test function.
Testing With done()
For callback-based functions, Mocha provides done().
Consider:
function getUser(callback) {
setTimeout(() => {
callback(null, {
id: 1,
name: "John"
});
}, 500);
}
Test it:
it("should return user information", function(done) {
getUser((error, user) => {
if (error) {
return done(error);
}
expect(user).to.have.property("id", 1);
expect(user).to.have.property("name", "John");
done();
});
});
When done() is called, Mocha knows that the asynchronous test has finished. Passing an error to done(error) tells Mocha the test failed.
Don’t Mix done() and Promises
Avoid code like this:
it("should return user", function(done) {
return getUser().then((user) => {
expect(user.name).to.equal("John");
done();
});
});
You’re using both:
Promise
+
done()
Mocha treats this as an overspecified completion mechanism and can fail the test.
Use either:
it("should return user", async function() {
const user = await getUser();
expect(user.name).to.equal("John");
});
or:
it("should return user", function() {
return getUser().then((user) => {
expect(user.name).to.equal("John");
});
});
Testing Rejected Promises
Suppose:
function getUser() {
return Promise.reject(
new Error("Unable to fetch user")
);
}
We can test it using try/catch:
it("should throw an error when user cannot be fetched", async function() {
try {
await getUser();
} catch (error) {
expect(error).to.be.instanceOf(Error);
expect(error.message)
.to.equal("Unable to fetch user");
}
});
A stronger test should also fail if the Promise unexpectedly resolves:
it("should reject when user cannot be fetched", async function() {
try {
await getUser();
expect.fail("Expected getUser() to reject");
} catch (error) {
expect(error.message)
.to.equal("Unable to fetch user");
}
});
Using Chai as Promised
If your project contains many Promise-based assertions, you can also use Chai as Promised, a Chai plugin designed for Promise assertions.
After installing it, you can configure Chai with the plugin and write fluent Promise assertions.
For example, the style can look like:
return expect(getUser())
.to.eventually.have.property("name", "John");
The advantage is that the test can express the expected asynchronous behavior directly.
However, for beginners, standard async/await with Chai assertions is often easier to understand.
Using Mocha Hooks
Real applications often require setup and cleanup before or after tests.
Mocha provides four common hooks:
before()
after()
beforeEach()
afterEach()
These hooks can prepare test data and clean it up afterward.
For example:
describe("Users", function() {
before(function() {
console.log("Runs once before all tests");
});
after(function() {
console.log("Runs once after all tests");
});
beforeEach(function() {
console.log("Runs before every test");
});
afterEach(function() {
console.log("Runs after every test");
});
});
Understanding before() and beforeEach()
before()
Runs once before the tests in the suite.
before(function() {
// setup
});
beforeEach()
Runs before every test.
beforeEach(function() {
// setup for each test
});
For example:
describe("Calculator", function() {
let calculator;
beforeEach(function() {
calculator = {
add: (a, b) => a + b
};
});
it("should add numbers", function() {
expect(calculator.add(2, 3))
.to.equal(5);
});
});
Organizing Tests With Nested describe()
As an application becomes larger, you can organize tests into nested groups.
describe("User Service", function() {
describe("createUser()", function() {
it("should create a user", function() {
});
it("should reject invalid data", function() {
});
});
describe("getUser()", function() {
it("should return a user", function() {
});
it("should return an error for an invalid ID", function() {
});
});
});
This makes test output easier to understand.
A Complete Node.js Example
Let’s build a small user service.
userService.js
function createUser(name, email) {
if (!name) {
throw new Error("Name is required");
}
if (!email) {
throw new Error("Email is required");
}
return {
id: 1,
name,
email
};
}
function getUser(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (!id) {
return reject(
new Error("User ID is required")
);
}
resolve({
id,
name: "John",
email: "john@example.com"
});
}, 300);
});
}
module.exports = {
createUser,
getUser
};
Testing the User Service
Create:
test/userService.test.js
Add:
const { expect } = require("chai");
const {
createUser,
getUser
} = require("../userService");
describe("User Service", function() {
describe("createUser()", function() {
it("should create a user", function() {
const user = createUser(
"John",
"john@example.com"
);
expect(user).to.deep.equal({
id: 1,
name: "John",
email: "john@example.com"
});
});
it("should throw an error when name is missing", function() {
expect(() => {
createUser("", "john@example.com");
}).to.throw("Name is required");
});
it("should throw an error when email is missing", function() {
expect(() => {
createUser("John", "");
}).to.throw("Email is required");
});
});
describe("getUser()", function() {
it("should return user information", async function() {
const user = await getUser(1);
expect(user).to.be.an("object");
expect(user).to.have.property("id", 1);
expect(user).to.have.property("name", "John");
});
it("should reject when ID is missing", async function() {
try {
await getUser();
expect.fail(
"Expected getUser() to reject"
);
} catch (error) {
expect(error.message)
.to.equal("User ID is required");
}
});
});
});
Run:
npm test
You now have tests for:
- Successful user creation
- Missing name
- Missing email
- Successful asynchronous user retrieval
- Asynchronous error handling
Testing API-Like Responses
Node.js applications frequently communicate with APIs.
Suppose a function returns:
const response = {
status: 200,
data: {
id: 1,
name: "John",
role: "admin"
}
};
You can test it with Chai:
expect(response).to.be.an("object");
expect(response)
.to.have.property("status", 200);
expect(response.data)
.to.have.property("id", 1);
expect(response.data)
.to.have.property("name", "John");
expect(response.data)
.to.have.property("role", "admin");
This approach is useful when testing service functions that transform or return API data.
Mocha and Chai With Express Applications
Mocha and Chai can also be part of a larger Node.js testing stack.
For example, an Express application might have:
Express
↓
Route
↓
Controller
↓
Service
↓
Database
You can use Mocha and Chai to test individual pieces of this flow.
For example, instead of testing the entire application at once, you can test:
Controller
↓
Expected response
or:
Service
↓
Expected result
For actual HTTP request testing, projects often add another library such as Supertest.
The important idea is to keep your tests focused on the behavior you’re trying to verify.
Mocha and Chai With CommonJS
The examples in this guide use CommonJS:
const { expect } = require("chai");
This is common in many Node.js projects.
Your application might use:
module.exports = {
add
};
and:
const { add } = require("../calculator");
Mocha and Chai With ES Modules
Mocha also supports Node.js native ES modules.
For example:
import { expect } from "chai";
import { add } from "../calculator.js";
describe("Calculator", function() {
it("should add two numbers", function() {
expect(add(10, 20)).to.equal(30);
});
});
Node.js can treat .mjs files as ES modules, or .js files can be treated as ES modules when "type": "module" is set in package.json. Mocha supports native ESM test files.
For example:
{
"type": "module"
}
Then you can use:
import { expect } from "chai";
instead of:
const { expect } = require("chai");
Common Mocha and Chai Mistakes
1. Forgetting to import Chai
Incorrect:
expect(result).to.equal(10);
Correct:
const { expect } = require("chai");
2. Comparing Objects With equal()
Incorrect:
expect(actual).to.equal(expected);
Use:
expect(actual).to.deep.equal(expected);
when you want to compare object contents.
3. Forgetting await
Incorrect:
const user = getUser();
expect(user.name).to.equal("John");
Correct:
const user = await getUser();
expect(user.name).to.equal("John");
4. Forgetting done()
If you’re using a callback-style asynchronous test:
it("should work", function(done) {
getData((data) => {
expect(data).to.exist;
done();
});
});
Without done(), Mocha doesn’t receive the callback signal that the asynchronous test has finished.
5. Mixing done() and Promises
Don’t use both completion mechanisms in the same test.
Choose:
async function()
or:
function(done)
or return a Promise.
6. Using Only Weak Assertions
Instead of:
expect(result).to.be.ok;
when you know the exact result:
expect(result).to.equal(100);
Chai’s own documentation recommends specific assertions where possible because they make the expected behavior clearer.
Best Practices for Mocha and Chai
1. Write Tests Around Behavior
Test what your application should do.
For example:
it("should calculate the order total", function() {
const total = calculateTotal(100, 2);
expect(total).to.equal(200);
});
The test describes behavior rather than implementation details.
2. Keep Tests Small
A test should ideally verify one meaningful behavior.
Avoid putting many unrelated scenarios into one test.
3. Use Clear Test Names
Good:
it("should reject when email is missing", function() {
});
Less useful:
it("test 2", function() {
});
A good test name makes a failure easier to understand.
4. Test Success and Failure
Don’t only test:
Valid input → Success
Also test:
Invalid input → Expected error
For example:
expect(() => createUser("", "john@example.com"))
.to.throw("Name is required");
5. Keep Tests Independent
A test should not depend on another test running first.
Instead of:
Test 1 creates data
↓
Test 2 uses Test 1 data
↓
Test 3 uses Test 2 data
use setup hooks:
beforeEach(function() {
// prepare clean test data
});
Mocha’s beforeEach() hook is specifically designed for setup that should happen before each test.
6. Use Specific Assertions
Prefer:
expect(user.name).to.equal("John");
over:
expect(user.name).to.be.ok;
The more specific the assertion, the more useful the test failure tends to be.
7. Don’t Test Implementation Details
If you change the internal implementation but the behavior stays the same, your tests should ideally continue to pass.
Focus on inputs and outputs.
Useful Chai Assertions Cheat Sheet
| Assertion | Purpose |
|---|---|
expect(value).to.equal(x) | Strict equality |
expect(value).to.deep.equal(x) | Deep comparison |
expect(value).to.not.equal(x) | Not equal |
expect(value).to.be.true | Exactly true |
expect(value).to.be.false | Exactly false |
expect(value).to.be.null | Checks null |
expect(value).to.be.undefined | Checks undefined |
expect(value).to.be.a("string") | Checks type |
expect(value).to.be.a("number") | Checks type |
expect(value).to.be.an("array") | Checks array |
expect(value).to.have.property("name") | Checks property |
expect(value).to.have.property("name", "John") | Checks property and value |
expect(array).to.include(value) | Checks inclusion |
expect(array).to.have.lengthOf(3) | Checks length |
expect(fn).to.throw() | Checks thrown error |
expect(value).to.be.greaterThan(10) | Checks greater value |
expect(value).to.be.lessThan(10) | Checks smaller value |
expect(value).to.be.within(1, 10) | Checks range |
Mocha vs Chai vs Jest
If you’re learning JavaScript testing, you may wonder how these tools compare.
| Tool | Main Purpose |
|---|---|
| Mocha | Test framework and test runner |
| Chai | Assertion library |
| Jest | Testing framework with assertions, mocks, and other built-in features |
With Mocha, you commonly choose additional libraries for assertions and other testing needs.
With Jest, many testing features are provided together.
Neither approach is automatically better for every project.
The important thing is understanding what each tool does.
When Should You Use Mocha and Chai?
Mocha and Chai are a good choice when you want flexibility over your testing stack.
They work well when you want to choose separate tools for:
- Test running
- Assertions
- HTTP testing
- Mocking
- Coverage
- Plugins
For example:
Mocha
+
Chai
+
Supertest
+
Sinon
+
c8
Each tool can solve a specific testing problem.
Running Your Tests
Once everything is configured, you can run:
npm test
or:
npx mocha
Mocha also provides a CLI with options for controlling how tests are executed. Running:
npx mocha --help
shows the available commands and options.
For example, Mocha provides an --async-only option that can enforce asynchronous-style tests by requiring callbacks or returned Promises.
A Recommended Project Structure
A Node.js project using Mocha and Chai could look like this:
my-node-app/
│
├── src/
│ ├── calculator.js
│ ├── userService.js
│ └── orderService.js
│
├── test/
│ ├── calculator.test.js
│ ├── userService.test.js
│ └── orderService.test.js
│
├── package.json
└── package-lock.json
This keeps application code and test code separate.
As your project grows, you can organize tests into additional folders based on features or modules.
Complete Testing Flow
The complete workflow looks like this:
Write Node.js code
↓
Create Mocha test
↓
Call the function
↓
Get actual result
↓
Chai checks expected result
↓
Mocha reports result
↓
Pass or Fail
For asynchronous code:
Mocha
↓
await Promise
↓
Application code
↓
Result
↓
Chai assertion
↓
Test result
This separation makes the roles of both libraries easy to understand.
Frequently Asked Questions
Is Chai required to use Mocha?
No.
Mocha can work with Node.js’s built-in assert module or other assertion libraries. Chai is one of the libraries that Mocha supports.
What is the difference between Mocha and Chai?
Mocha runs and organizes tests.
Chai provides assertions that verify whether the results are correct.
Which Chai style should beginners use?
The expect style is a good starting point:
expect(result).to.equal(10);
It provides a readable, chainable syntax.
Can Mocha and Chai test asynchronous code?
Yes.
Mocha supports callbacks, Promises, and async/await, while Chai can assert the values returned by those operations.
Can I use Chai without Mocha?
Yes.
Chai is an assertion library and can be used with different testing environments and frameworks.
Can I use Mocha without Chai?
Yes.
Mocha doesn’t require Chai. You can use Node.js’s built-in assert or another assertion library.
Should I use Mocha and Chai or Jest?
It depends on your project.
Mocha and Chai give you a modular testing stack, while Jest provides many testing features in one framework.
If you’re specifically learning Node.js testing, understanding Mocha and Chai is valuable because it teaches you the difference between a test runner/framework and an assertion library.
Conclusion
Mocha.js and Chai are a powerful combination for testing Node.js applications.
Mocha handles the test structure, execution, asynchronous flow, and hooks, while Chai provides expressive assertions.
The basic combination looks like this:
const { expect } = require("chai");
describe("Calculator", function() {
it("should add two numbers", function() {
const result = 10 + 20;
expect(result).to.equal(30);
});
});
Once you understand the basics, you can use the same approach to test:
- Functions
- Objects
- Arrays
- Errors
- Promises
async/await- Node.js services
- API responses
- Express applications
The most important concept to remember is:
Mocha → Runs and organizes tests
Chai → Checks whether results are correct
Together, they give you a flexible foundation for writing reliable Node.js tests.




