Unit Testing with Jest
Setting Up Jest
Install Jest in a Node.js project:
npm install --save-dev jest
Add the test script to package.json:
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
}
For TypeScript or ES Modules, also install:
npm install --save-dev babel-jest @babel/core @babel/preset-env
Run your tests:
npm test # run once
npm run test:watch # re-run on file change (great for TDD)
npm run test:coverage # run with coverage report
Your First Test
Create a file to test:
function add(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, divide };
Create the test file (convention: same name, .test.js suffix):
const { add, divide } = require("./math");
test("adds two numbers", () => {
expect(add(2, 3)).toBe(5);
});
test("divides two numbers", () => {
expect(divide(10, 2)).toBe(5);
});
test("throws when dividing by zero", () => {
expect(() => divide(10, 0)).toThrow("Cannot divide by zero");
});
Run npm test — you should see all 3 tests pass.
Test Structure: describe and test
Group related tests with describe blocks for readable output:
describe("add()", () => {
test("adds positive numbers", () => {
expect(add(1, 2)).toBe(3);
});
test("adds negative numbers", () => {
expect(add(-1, -2)).toBe(-3);
});
test("adds zero", () => {
expect(add(5, 0)).toBe(5);
});
});
describe("divide()", () => {
test("divides evenly", () => {
expect(divide(10, 2)).toBe(5);
});
test("returns a decimal", () => {
expect(divide(1, 3)).toBeCloseTo(0.333);
});
});
Output becomes:
add()
✓ adds positive numbers
✓ adds negative numbers
✓ adds zero
divide()
✓ divides evenly
✓ returns a decimal
Common Matchers
Matchers are the expect(value).toBe... part of each assertion:
Equality
expect(2 + 2).toBe(4); // strict equality (===)
expect({ a: 1 }).toEqual({ a: 1 }); // deep equality (for objects/arrays)
expect([1, 2]).toEqual([1, 2]);
toBe vs toEqualUse toBe for primitives (numbers, strings, booleans). Use toEqual for objects and arrays — toBe compares references, so expect({}).toBe({}) will fail even though the contents are identical.
Truthiness
expect(null).toBeNull();
expect(undefined).toBeUndefined();
expect("hello").toBeDefined();
expect(true).toBeTruthy();
expect(0).toBeFalsy();
expect("").toBeFalsy();
Numbers
expect(10).toBeGreaterThan(5);
expect(3).toBeLessThanOrEqual(3);
expect(0.1 + 0.2).toBeCloseTo(0.3); // floating point safe
Strings
expect("hello world").toContain("world");
expect("hello").toMatch(/^h/);
Arrays and Objects
expect([1, 2, 3]).toContain(2);
expect({ name: "Rizwan", age: 25 }).toMatchObject({ name: "Rizwan" }); // partial match
expect([1, 2, 3]).toHaveLength(3);
Errors
expect(() => JSON.parse("invalid")).toThrow();
expect(() => JSON.parse("invalid")).toThrow(SyntaxError);
expect(() => { throw new Error("oops") }).toThrow("oops");
Negation
expect(5).not.toBe(3);
expect(null).not.toBeDefined();
Testing Async Code
Promises
// Return the promise — Jest waits for it
test("fetches a user", () => {
return fetchUser(1).then((user) => {
expect(user.name).toBe("Rizwan");
});
});
Async/Await (preferred)
test("fetches a user", async () => {
const user = await fetchUser(1);
expect(user.name).toBe("Rizwan");
});
test("throws on invalid user", async () => {
await expect(fetchUser(999)).rejects.toThrow("User not found");
});
Real Example: Testing an Async Service
const axios = require("axios");
async function getUser(id) {
const res = await axios.get(`https://api.example.com/users/${id}`);
return res.data;
}
module.exports = { getUser };
const axios = require("axios");
const { getUser } = require("./userService");
jest.mock("axios"); // mock the entire axios module
test("returns user data", async () => {
axios.get.mockResolvedValue({ data: { id: 1, name: "Rizwan" } });
const user = await getUser(1);
expect(axios.get).toHaveBeenCalledWith("https://api.example.com/users/1");
expect(user.name).toBe("Rizwan");
});
test("propagates API errors", async () => {
axios.get.mockRejectedValue(new Error("Network error"));
await expect(getUser(1)).rejects.toThrow("Network error");
});
Setup and Teardown
Run code before/after each test or the entire test suite:
describe("Database tests", () => {
let db;
beforeAll(async () => {
db = await connectToDatabase(); // runs once before all tests in this describe
});
afterAll(async () => {
await db.disconnect(); // cleanup after all tests
});
beforeEach(async () => {
await db.clearTable("users"); // fresh state before each test
});
test("creates a user", async () => {
const user = await db.createUser({ name: "Rizwan" });
expect(user.id).toBeDefined();
});
test("finds a user", async () => {
await db.createUser({ name: "Ali" });
const user = await db.findUser({ name: "Ali" });
expect(user).not.toBeNull();
});
});
Skipping and Focusing Tests
test.skip("this test is skipped", () => { ... });
test.todo("implement this test later");
// Only run this test (useful for debugging a single failing test)
test.only("this is the only test that runs", () => { ... });
// Same for describe blocks
describe.skip("entire block skipped", () => { ... });
describe.only("only this block runs", () => { ... });
.onlytest.only and describe.only are debug tools — they cause all other tests to be silently skipped. Always remove them before committing. Most CI setups (and ESLint's jest/no-focused-tests rule) will catch this.
Code Coverage
npm run test:coverage
Jest generates a coverage report showing which lines, branches, and functions are tested:
File | % Stmts | % Branch | % Funcs | % Lines
math.js | 100 | 100 | 100 | 100
userService.js| 80 | 50 | 100 | 80
Coverage is a guide, not a goal. 100% coverage doesn't mean zero bugs — it means every line was executed, not that every case was verified. Focus coverage on business-critical paths.
File Naming Conventions
Jest finds test files matching these patterns by default:
src/
math.js
math.test.js ← colocated (recommended)
__tests__/
math.test.js ← also works
userService.js
userService.spec.js ← .spec.js also works
Colocating tests next to source files (.test.js beside the file it tests) makes it easy to find related tests and notice when a file has no tests.