JavaScript Testing with Jest: Unit Tests, Mocks, Coverage and Best Practices
Writing tests is one of the clearest signals of code quality and professional maturity. Jest is the most widely used JavaScript testing framework β it works for plain JavaScript, Node.js APIs, and React components. This guide teaches you to write tests that are fast, reliable, and actually useful.
Why Testing Matters
Tests catch regressions (bugs you accidentally reintroduce), document expected behavior, and give you confidence to refactor. Good tests are an investment that pays back every time you change code without breaking production.
Jest Basics
bashnpm install --save-dev jest
javascript// math.js export function add(a, b) { return a + b; } export function divide(a, b) { if (b === 0) throw new Error("Division by zero"); return a / b; } // math.test.js import { add, divide } from "./math"; describe("math utilities", () => { describe("add", () => { test("adds two positive numbers", () => { expect(add(1, 2)).toBe(3); }); test("adds negative numbers", () => { expect(add(-1, -2)).toBe(-3); }); test("handles zero", () => { expect(add(5, 0)).toBe(5); }); }); describe("divide", () => { test("divides correctly", () => { expect(divide(10, 2)).toBe(5); }); test("throws on division by zero", () => { expect(() => divide(10, 0)).toThrow("Division by zero"); }); }); });
Run tests:
bashnpx jest npx jest --watch # re-run on file change npx jest math.test.js # run specific file npx jest --coverage # include coverage report
Common Matchers
javascript// Equality expect(value).toBe(42); // strict equality (===) expect(obj).toEqual({ a: 1 }); // deep equality for objects/arrays expect(value).not.toBe(null); // Truthiness expect(value).toBeTruthy(); expect(value).toBeFalsy(); expect(value).toBeNull(); expect(value).toBeUndefined(); expect(value).toBeDefined(); // Numbers expect(0.1 + 0.2).toBeCloseTo(0.3); // floating point expect(value).toBeGreaterThan(3); expect(value).toBeLessThanOrEqual(10); // Strings expect("Hello World").toContain("World"); expect("hello").toMatch(/^hel/); // Arrays expect([1, 2, 3]).toContain(2); expect([1, 2, 3]).toHaveLength(3); expect(arr).toEqual(expect.arrayContaining([1, 3])); // Objects expect(obj).toHaveProperty("name", "Alice"); expect(obj).toMatchObject({ name: "Alice" }); // partial match // Errors expect(() => riskyFn()).toThrow(); expect(() => riskyFn()).toThrow("specific message"); expect(() => riskyFn()).toThrow(TypeError);
Testing Async Code
Promises and async/await
javascriptimport { fetchUser } from "./api"; // With async/await (cleanest) test("fetches user by id", async () => { const user = await fetchUser(1); expect(user).toHaveProperty("name"); expect(user.id).toBe(1); }); // Test that a promise rejects test("throws on invalid id", async () => { await expect(fetchUser(-1)).rejects.toThrow("Invalid ID"); }); // With .resolves / .rejects matchers test("resolves with user data", () => { return expect(fetchUser(1)).resolves.toMatchObject({ id: 1 }); });
Always return or await async assertions β if you forget, the test passes before the assertion runs.
Mocking
jest.fn()
Create a mock function that records calls:
javascripttest("calls the callback with the result", () => { const callback = jest.fn(); processData([1, 2, 3], callback); expect(callback).toHaveBeenCalledTimes(3); expect(callback).toHaveBeenCalledWith(1); expect(callback).toHaveBeenLastCalledWith(3); }); // Mock return value const mockFn = jest.fn().mockReturnValue(42); const mockFnOnce = jest.fn() .mockReturnValueOnce("first") .mockReturnValueOnce("second") .mockReturnValue("default");
jest.mock() β mock entire modules
javascript// emailService.js export async function sendEmail(to, subject, body) { // real implementation calls an email API } // user.service.test.js import { sendEmail } from "./emailService"; import { registerUser } from "./user.service"; jest.mock("./emailService"); // replaces module with auto-mock test("sends welcome email after registration", async () => { sendEmail.mockResolvedValue({ success: true }); await registerUser({ email: "alice@example.com", password: "secret" }); expect(sendEmail).toHaveBeenCalledWith( "alice@example.com", "Welcome!", expect.stringContaining("alice@example.com") ); }); afterEach(() => { jest.clearAllMocks(); // reset call counts between tests });
Mocking fetch / HTTP
javascriptglobal.fetch = jest.fn(); test("fetches and parses JSON", async () => { fetch.mockResolvedValue({ ok: true, json: async () => ({ id: 1, name: "Alice" }), }); const user = await getUser(1); expect(fetch).toHaveBeenCalledWith("/api/users/1"); expect(user.name).toBe("Alice"); });
Spying on existing methods
javascriptimport * as db from "./database"; test("calls db.save with the right data", async () => { const saveSpy = jest.spyOn(db, "save").mockResolvedValue({ id: 1 }); await createUser({ name: "Alice" }); expect(saveSpy).toHaveBeenCalledWith( expect.objectContaining({ name: "Alice" }) ); saveSpy.mockRestore(); // restore original implementation });
Setup and Teardown
javascriptdescribe("UserService", () => { let service; beforeAll(async () => { // runs once before all tests in this describe await db.connect(); }); afterAll(async () => { // runs once after all tests in this describe await db.disconnect(); }); beforeEach(() => { // runs before each test service = new UserService(); }); afterEach(() => { // runs after each test -- clean up mocks, state jest.clearAllMocks(); }); test("creates a user", async () => { ... }); });
Testing React Components
bashnpm install --save-dev @testing-library/react @testing-library/jest-dom
jsx// Button.jsx export function Button({ label, onClick, disabled = false }) { return ( <button onClick={onClick} disabled={disabled}> {label} </button> ); } // Button.test.jsx import { render, screen, fireEvent } from "@testing-library/react"; import { Button } from "./Button"; test("renders button with label", () => { render(<Button label="Click me" onClick={() => {}} />); expect(screen.getByText("Click me")).toBeInTheDocument(); }); test("calls onClick when clicked", () => { const handleClick = jest.fn(); render(<Button label="Submit" onClick={handleClick} />); fireEvent.click(screen.getByText("Submit")); expect(handleClick).toHaveBeenCalledTimes(1); }); test("is disabled when disabled prop is true", () => { render(<Button label="Save" onClick={() => {}} disabled />); expect(screen.getByText("Save")).toBeDisabled(); });
Testing async React components
jsximport { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { UserProfile } from "./UserProfile"; jest.mock("./api", () => ({ fetchUser: jest.fn().mockResolvedValue({ id: 1, name: "Alice" }), })); test("displays user name after loading", async () => { render(<UserProfile userId={1} />); expect(screen.getByText("Loading...")).toBeInTheDocument(); await waitFor(() => { expect(screen.getByText("Alice")).toBeInTheDocument(); }); });
Code Coverage
bashnpx jest --coverage
Jest generates a coverage report showing which lines, branches, and functions were exercised:
codeFile | % Stmts | % Branch | % Funcs | % Lines -------------|---------|----------|---------|-------- math.js | 100 | 100 | 100 | 100 user.service | 85 | 72 | 90 | 85
A reasonable coverage target for business-critical code is 80%+. Do not chase 100% β some code is not worth testing.
Testing Best Practices
Name tests as specifications:
javascript// Bad test("test 1", () => { ... }); // Good test("sends a password reset email when the user exists", () => { ... }); test("throws UserNotFoundError when the email is not registered", () => { ... });
One assertion per concept β tests should tell you exactly what failed:
javascript// Bad: hard to know which assertion failed test("user creation", async () => { const user = await createUser(data); expect(user.id).toBeDefined(); expect(user.email).toBe("alice@example.com"); expect(user.createdAt).toBeDefined(); expect(mockEmail).toHaveBeenCalled(); }); // Better: split by concern test("returns a user with an id", async () => { ... }); test("sends a welcome email", async () => { ... });
Avoid testing implementation details β test behavior, not internals. If you refactor the internals and all tests still pass, that is a good sign.
Practice on Froquiz
Testing knowledge is increasingly expected at mid-level and senior developer interviews. Test your JavaScript and Node.js knowledge on Froquiz β we cover async, OOP, and modern JavaScript concepts.
Summary
describegroups related tests;test/itdefines a single test case- Use
toBefor primitives;toEqualfor objects and arrays (deep equality) - Always
awaitorreturnasync assertions β forgotten awaits cause false passes jest.mock()replaces entire modules;jest.spyOn()intercepts specific methodsbeforeEach/afterEachset up and tear down per-test state;beforeAll/afterAllfor expensive shared setup- Test React components with Testing Library β query by accessible roles and text, not implementation
- Name tests as specifications β a failing test should clearly describe what is broken