Skip to main content

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:

src/math.js
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):

src/math.test.js
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]);
tip
toBe vs toEqual

Use 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

src/userService.js
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 };
src/userService.test.js
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", () => { ... });
caution
Don't commit .only

test.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.