Node.js Testing with Jest
Testing is the practice of writing code that automatically verifies that other code works correctly. Without tests, every change to an application requires manual testing — which is slow, error-prone, and does not scale. Automated tests catch bugs early, provide confidence when making changes, and serve as documentation of how code is expected to behave.
Jest is a popular, zero-configuration JavaScript testing framework created by Facebook. It works seamlessly with Node.js applications and provides everything needed to write and run tests: a test runner, assertion library, mocking tools, and code coverage reporting — all in one package.
Types of Tests
- Unit Tests: Test individual functions or modules in isolation.
- Integration Tests: Test how multiple parts of the application work together (e.g., a route handler with a database).
- End-to-End (E2E) Tests: Test the full user flow through the entire application.
This topic focuses on unit and integration testing — the most common in Node.js development.
Installing Jest
npm install jest --save-dev
Add a test script to package.json:
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
Writing the First Test
The Function to Test – math.js
// math.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("Cannot divide by zero");
return a / b;
}
module.exports = { add, subtract, multiply, divide };
The Test File – math.test.js
Jest automatically discovers test files with the .test.js or .spec.js suffix, or files inside a __tests__ folder.
// math.test.js
const { add, subtract, multiply, divide } = require('./math');
describe('Math Functions', function() {
test('add() should return the sum of two numbers', function() {
expect(add(3, 4)).toBe(7);
expect(add(-1, 1)).toBe(0);
expect(add(0, 0)).toBe(0);
});
test('subtract() should return the difference of two numbers', function() {
expect(subtract(10, 4)).toBe(6);
expect(subtract(0, 5)).toBe(-5);
});
test('multiply() should return the product of two numbers', function() {
expect(multiply(3, 5)).toBe(15);
expect(multiply(-2, 4)).toBe(-8);
});
test('divide() should return the quotient of two numbers', function() {
expect(divide(10, 2)).toBe(5);
expect(divide(9, 3)).toBe(3);
});
test('divide() should throw an error when dividing by zero', function() {
expect(() => divide(10, 0)).toThrow("Cannot divide by zero");
});
});
Run tests:
npm test
Understanding the Test Structure
describe()— Groups related tests together under a label. It is optional but improves readability.test()orit()— Defines a single test case with a description and a function.expect(value)— Creates an assertion about a value.- Matcher — A method chained onto
expect()that checks a condition (e.g.,.toBe(),.toEqual()).
Common Jest Matchers
| Matcher | Description | Example |
|---|---|---|
.toBe(value) | Strict equality (===) | expect(1 + 1).toBe(2) |
.toEqual(value) | Deep equality for objects/arrays | expect({a:1}).toEqual({a:1}) |
.toBeTruthy() | Value is truthy | expect("hello").toBeTruthy() |
.toBeFalsy() | Value is falsy | expect(0).toBeFalsy() |
.toBeNull() | Value is null | expect(null).toBeNull() |
.toBeGreaterThan(n) | Value is greater than n | expect(5).toBeGreaterThan(3) |
.toContain(item) | Array or string contains item | expect([1,2,3]).toContain(2) |
.toThrow() | Function throws an error | expect(() => fn()).toThrow() |
.toHaveLength(n) | Array or string has length n | expect([1,2,3]).toHaveLength(3) |
.toHaveProperty(key) | Object has a specific property | expect({name:'A'}).toHaveProperty('name') |
Testing Asynchronous Code
Testing with Promises
function fetchUser(id) {
return new Promise((resolve, reject) => {
if (id > 0) {
resolve({ id, name: 'Alice' });
} else {
reject(new Error('Invalid ID'));
}
});
}
test('fetchUser() resolves with a user object', async function() {
const user = await fetchUser(1);
expect(user).toEqual({ id: 1, name: 'Alice' });
expect(user).toHaveProperty('name');
});
test('fetchUser() rejects for invalid ID', async function() {
await expect(fetchUser(-1)).rejects.toThrow('Invalid ID');
});
Mocking Functions and Modules
Mocking replaces real functions or modules with fake versions during testing. This is essential for isolating units and avoiding real database calls or API requests in unit tests.
Mocking a Function
// order.js
function sendEmail(to, message) {
// In real life, this sends an actual email
console.log("Sending email to:", to);
}
function placeOrder(product, email, mailer) {
const confirmationCode = Math.floor(Math.random() * 10000);
mailer(email, `Order confirmed: ${product} (Code: ${confirmationCode})`);
return { product, email, confirmationCode };
}
module.exports = { placeOrder, sendEmail };
// order.test.js
const { placeOrder } = require('./order');
test('placeOrder() calls the mailer with correct arguments', function() {
const mockMailer = jest.fn(); // Create a mock function
const order = placeOrder('Laptop', 'user@example.com', mockMailer);
expect(mockMailer).toHaveBeenCalledTimes(1);
expect(mockMailer).toHaveBeenCalledWith(
'user@example.com',
expect.stringContaining('Laptop')
);
expect(order.product).toBe('Laptop');
});
Setup and Teardown
describe('Database Tests', function() {
let db;
// Runs before each test
beforeEach(function() {
db = { users: [{ id: 1, name: 'Alice' }] };
});
// Runs after each test
afterEach(function() {
db = null;
});
test('DB has one user initially', function() {
expect(db.users).toHaveLength(1);
});
test('Can add a user', function() {
db.users.push({ id: 2, name: 'Bob' });
expect(db.users).toHaveLength(2);
});
});
Generating a Coverage Report
npm run test:coverage
Jest generates a detailed report showing how much of the codebase is covered by tests — broken down by file, function, line, and branch coverage. A coverage percentage above 70–80% is generally considered good for production projects.
Key Points
- Testing ensures code behaves as expected and prevents regressions when changes are made.
- Jest is a zero-configuration testing framework that provides a test runner, assertions, mocks, and coverage in one tool.
- Tests are organized with
describe()and defined withtest()orit(). Assertions useexpect(value).matcher(). - Asynchronous tests use
async/awaitwith.resolvesand.rejectsfor Promise-based code. - Mock functions (
jest.fn()) replace real implementations during tests to isolate units. beforeEachandafterEachhandle setup and cleanup around individual tests.- Code coverage reports show exactly which parts of the code are not being tested.
