Unit Testing with Jest
📖 Concept
Jest is the most popular JavaScript testing framework, created by Facebook. It's zero-config, batteries-included, and works seamlessly with Node.js.
Why Jest?
- Built-in assertions, mocking, code coverage
- Snapshot testing
- Parallel test execution
- Watch mode for development
- Rich ecosystem and community
Testing pyramid:
/ E2E \ ← Few (slow, expensive)
/ Integration \ ← Some (medium speed)
/ Unit Tests \ ← Many (fast, cheap)
Jest concepts:
- Test suite —
describe('module', () => { ... }) - Test case —
test('should do X', () => { ... })orit('should...') - Assertion —
expect(value).toBe(expected) - Mock — Replace real functions/modules with controlled versions
- Spy — Watch a function without replacing it
Assertion methods:
| Method | Use Case |
|---|---|
.toBe(value) |
Strict equality (===) |
.toEqual(object) |
Deep equality (objects/arrays) |
.toBeTruthy() / .toBeFalsy() |
Boolean checks |
.toThrow(error) |
Expect an error to be thrown |
.toHaveBeenCalled() |
Check if mock was called |
.toHaveBeenCalledWith(args) |
Check mock call arguments |
.resolves / .rejects |
Async assertions |
🏠 Real-world analogy: Unit tests are like quality control inspections on an assembly line. Each component (unit) is tested individually before assembly. If a bolt (function) is defective, you catch it before it's part of the finished product (application).
💻 Code Example
1// Unit Testing with Jest — Complete Guide23// === src/services/userService.js ===4class UserService {5 constructor(userRepository, emailService) {6 this.userRepository = userRepository;7 this.emailService = emailService;8 }910 async createUser(data) {11 if (!data.email || !data.name) {12 throw new Error("Name and email are required");13 }1415 const existing = await this.userRepository.findByEmail(data.email);16 if (existing) {17 throw new Error("Email already registered");18 }1920 const user = await this.userRepository.create({21 ...data,22 createdAt: new Date(),23 });2425 await this.emailService.sendWelcome(user.email, user.name);2627 return user;28 }2930 async getUserById(id) {31 const user = await this.userRepository.findById(id);32 if (!user) throw new Error("User not found");33 return user;34 }35}3637module.exports = UserService;3839// === tests/unit/userService.test.js ===40const UserService = require("../../src/services/userService");4142describe("UserService", () => {43 let userService;44 let mockUserRepo;45 let mockEmailService;4647 // Setup before each test48 beforeEach(() => {49 // Create mocks50 mockUserRepo = {51 findByEmail: jest.fn(),52 findById: jest.fn(),53 create: jest.fn(),54 };55 mockEmailService = {56 sendWelcome: jest.fn().mockResolvedValue(true),57 };5859 userService = new UserService(mockUserRepo, mockEmailService);60 });6162 // Clear mocks after each test63 afterEach(() => {64 jest.clearAllMocks();65 });6667 describe("createUser", () => {68 const validUserData = { name: "Alice", email: "alice@example.com" };6970 test("should create a user successfully", async () => {71 // Arrange72 mockUserRepo.findByEmail.mockResolvedValue(null); // No existing user73 mockUserRepo.create.mockResolvedValue({ id: 1, ...validUserData });7475 // Act76 const user = await userService.createUser(validUserData);7778 // Assert79 expect(user).toEqual(expect.objectContaining({ name: "Alice" }));80 expect(mockUserRepo.create).toHaveBeenCalledTimes(1);81 expect(mockEmailService.sendWelcome).toHaveBeenCalledWith(82 "alice@example.com",83 "Alice"84 );85 });8687 test("should throw if email is missing", async () => {88 await expect(userService.createUser({ name: "Alice" })).rejects.toThrow(89 "Name and email are required"90 );91 expect(mockUserRepo.create).not.toHaveBeenCalled();92 });9394 test("should throw if email already exists", async () => {95 mockUserRepo.findByEmail.mockResolvedValue({ id: 1, email: validUserData.email });9697 await expect(userService.createUser(validUserData)).rejects.toThrow(98 "Email already registered"99 );100 expect(mockUserRepo.create).not.toHaveBeenCalled();101 });102103 test("should still create user if welcome email fails", async () => {104 mockUserRepo.findByEmail.mockResolvedValue(null);105 mockUserRepo.create.mockResolvedValue({ id: 1, ...validUserData });106 mockEmailService.sendWelcome.mockRejectedValue(new Error("SMTP error"));107108 await expect(userService.createUser(validUserData)).rejects.toThrow("SMTP error");109 });110 });111112 describe("getUserById", () => {113 test("should return user when found", async () => {114 const mockUser = { id: 1, name: "Alice" };115 mockUserRepo.findById.mockResolvedValue(mockUser);116117 const user = await userService.getUserById(1);118119 expect(user).toEqual(mockUser);120 expect(mockUserRepo.findById).toHaveBeenCalledWith(1);121 });122123 test("should throw when user not found", async () => {124 mockUserRepo.findById.mockResolvedValue(null);125126 await expect(userService.getUserById(999)).rejects.toThrow("User not found");127 });128 });129});130131// === jest.config.js ===132module.exports = {133 testEnvironment: "node",134 coverageDirectory: "coverage",135 coverageThreshold: {136 global: { branches: 80, functions: 80, lines: 80, statements: 80 },137 },138 testMatch: ["**/tests/**/*.test.js"],139 setupFilesAfterSetup: ["./tests/setup.js"],140};
🏋️ Practice Exercise
Exercises:
- Write unit tests for a Calculator class with
add,subtract,multiply,divide— cover edge cases - Test an async service with mocked dependencies — use
jest.fn()for repository and external services - Achieve 90%+ code coverage on a service module — use
jest --coverageand fill gaps - Write tests using
beforeEach,afterEach,beforeAll,afterAllfor setup and cleanup - Test error handling — verify that functions throw specific errors with specific messages
- Use Jest's snapshot testing to test a configuration generator function
⚠️ Common Mistakes
Testing implementation details instead of behavior — don't test that a specific internal method was called; test the observable output
Not isolating units — unit tests should mock all dependencies; hitting real databases or APIs makes them integration tests
Forgetting to clear mocks between tests — stale mock state from one test affects the next; use
jest.clearAllMocks()inafterEachWriting tests that depend on execution order — each test should be independent; use
beforeEachfor setup, not shared mutable stateOnly testing the happy path — test edge cases, error conditions, boundary values, and empty inputs
💼 Interview Questions
🎤 Mock Interview
Mock interview is powered by AI for Unit Testing with Jest. Login to unlock this feature.