Unit Testing with Jest
Jest is a popular JavaScript testing framework that makes writing and running tests easy and enjoyable.
Installation and Setup
Install Jest
npm install --save-dev jest
Configure package.json
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
}
Create Test File
// math.test.js
describe("Math operations", () => {
test("adds two numbers", () => {
expect(2 + 2).toBe(4);
});
});
Run Tests
npm test
Basic Test Structure
Test Syntax
test("description", () => {
// Test code
});
// Or using it()
it("description", () => {
// Test code
});
Describe Blocks
describe("Calculator", () => {
test("adds numbers", () => {
expect(add(2, 3)).toBe(5);
});
test("subtracts numbers", () => {
expect(subtract(5, 3)).toBe(2);
});
});
Matchers
Equality
test("equality matchers", () => {
expect(4).toBe(4); // Strict equality
expect({ a: 1 }).toEqual({ a: 1 }); // Deep equality
expect(null).toBeNull();
expect(undefined).toBeUndefined();
expect(true).toBeDefined();
});
Truthiness
test("truthiness", () => {
expect(true).toBeTruthy();
expect(false).toBeFalsy();
expect(1).toBeTruthy();
expect(0).toBeFalsy();
});
Numbers
test("number matchers", () => {
expect(4).toBeGreaterThan(3);
expect(3).toBeGreaterThanOrEqual(3);
expect(2).toBeLessThan(3);
expect(3).toBeLessThanOrEqual(3);
expect(0.1 + 0.2).toBeCloseTo(0.3);
});
Strings
test("string matchers", () => {
expect("hello").toMatch(/ell/);
expect("hello").toMatch("ell");
expect("hello").toContain("ell");
});
Arrays and Objects
test("array and object matchers", () => {
expect([1, 2, 3]).toContain(2);
expect([1, 2, 3]).toHaveLength(3);
expect({ a: 1 }).toHaveProperty("a");
expect({ a: 1 }).toHaveProperty("a", 1);
});
Exceptions
test("exception matchers", () => {
expect(() => {
throw new Error("Test error");
}).toThrow();
expect(() => {
throw new Error("Test error");
}).toThrow("Test error");
expect(() => {
throw new Error("Test error");
}).toThrow(Error);
});
Setup and Teardown
beforeEach and afterEach
describe("Database", () => {
let db;
beforeEach(() => {
db = new Database();
db.connect();
});
afterEach(() => {
db.disconnect();
});
test("saves data", () => {
db.save("key", "value");
expect(db.get("key")).toBe("value");
});
});
beforeAll and afterAll
describe("API", () => {
let server;
beforeAll(() => {
server = startServer();
});
afterAll(() => {
server.stop();
});
test("fetches data", async () => {
const data = await fetch("/api/data");
expect(data).toBeDefined();
});
});
Practical Examples
Testing Functions
// math.js
export function add(a, b) {
return a + b;
}
export function multiply(a, b) {
return a * b;
}
// math.test.js
import { add, multiply } from "./math";
describe("Math functions", () => {
test("add returns sum", () => {
expect(add(2, 3)).toBe(5);
expect(add(-1, 1)).toBe(0);
expect(add(0, 0)).toBe(0);
});
test("multiply returns product", () => {
expect(multiply(2, 3)).toBe(6);
expect(multiply(-2, 3)).toBe(-6);
expect(multiply(0, 5)).toBe(0);
});
});
Testing Classes
// User.js
export class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
getInfo() {
return `${this.name} (${this.email})`;
}
isValidEmail() {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.email);
}
}
// User.test.js
import { User } from "./User";
describe("User class", () => {
let user;
beforeEach(() => {
user = new User("Alice", "[email protected]");
});
test("creates user with name and email", () => {
expect(user.name).toBe("Alice");
expect(user.email).toBe("[email protected]");
});
test("getInfo returns formatted string", () => {
expect(user.getInfo()).toBe("Alice ([email protected])");
});
test("isValidEmail validates email", () => {
expect(user.isValidEmail()).toBe(true);
user.email = "invalid";
expect(user.isValidEmail()).toBe(false);
});
});
Testing Async Functions
// api.js
export async function fetchUser(id) {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
// api.test.js
import { fetchUser } from "./api";
describe("API functions", () => {
test("fetchUser returns user data", async () => {
const user = await fetchUser(1);
expect(user).toHaveProperty("id");
expect(user).toHaveProperty("name");
});
});
Testing with Mocks
// database.js
export class Database {
async save(key, value) {
// Actual database operation
}
}
// service.js
export class UserService {
constructor(db) {
this.db = db;
}
async createUser(name, email) {
const user = { name, email };
await this.db.save("user", user);
return user;
}
}
// service.test.js
import { UserService } from "./service";
describe("UserService", () => {
test("createUser saves to database", async () => {
const mockDb = {
save: jest.fn()
};
const service = new UserService(mockDb);
const user = await service.createUser("Alice", "[email protected]");
expect(mockDb.save).toHaveBeenCalledWith("user", user);
expect(mockDb.save).toHaveBeenCalledTimes(1);
});
});
Testing with Spies
// calculator.js
export class Calculator {
add(a, b) {
return a + b;
}
calculate(a, b, operation) {
return operation(a, b);
}
}
// calculator.test.js
import { Calculator } from "./calculator";
describe("Calculator", () => {
test("calculate calls operation", () => {
const calc = new Calculator();
const spy = jest.spyOn(calc, "add");
calc.calculate(2, 3, calc.add);
expect(spy).toHaveBeenCalledWith(2, 3);
spy.mockRestore();
});
});
Test Coverage
Generate Coverage Report
npm run test:coverage
Coverage Thresholds
{
"jest": {
"collectCoverageFrom": [
"src/**/*.js",
"!src/index.js"
],
"coverageThreshold": {
"global": {
"branches": 80,
"functions": 80,
"lines": 80,
"statements": 80
}
}
}
}
Best Practices
Write Descriptive Test Names
// Good
test("returns sum of two positive numbers", () => {
expect(add(2, 3)).toBe(5);
});
// Avoid
test("add works", () => {
expect(add(2, 3)).toBe(5);
});
Test One Thing Per Test
// Good - focused test
test("add returns correct sum", () => {
expect(add(2, 3)).toBe(5);
});
// Avoid - testing multiple things
test("add and multiply work", () => {
expect(add(2, 3)).toBe(5);
expect(multiply(2, 3)).toBe(6);
});
Use Arrange-Act-Assert Pattern
test("user can login", () => {
// Arrange
const user = new User("alice", "password123");
// Act
const result = user.login("alice", "password123");
// Assert
expect(result).toBe(true);
});
Mock External Dependencies
// Good - mock external API
jest.mock("./api");
test("service fetches data", async () => {
const mockData = { id: 1, name: "Alice" };
api.fetchUser.mockResolvedValue(mockData);
const result = await service.getUser(1);
expect(result).toEqual(mockData);
});
Summary
- Jest: JavaScript testing framework
- test(): define a test
- expect(): make assertions
- Matchers: toBe(), toEqual(), toContain(), etc.
- Setup/Teardown: beforeEach(), afterEach()
- Mocks: jest.fn(), jest.mock()
- Spies: jest.spyOn()
- Coverage: measure code coverage
- Best practice: write focused, descriptive tests
Comments