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 suitedescribe('module', () => { ... })
  • Test casetest('should do X', () => { ... }) or it('should...')
  • Assertionexpect(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

codeTap to expand ⛶
1// Unit Testing with Jest — Complete Guide
2
3// === src/services/userService.js ===
4class UserService {
5 constructor(userRepository, emailService) {
6 this.userRepository = userRepository;
7 this.emailService = emailService;
8 }
9
10 async createUser(data) {
11 if (!data.email || !data.name) {
12 throw new Error("Name and email are required");
13 }
14
15 const existing = await this.userRepository.findByEmail(data.email);
16 if (existing) {
17 throw new Error("Email already registered");
18 }
19
20 const user = await this.userRepository.create({
21 ...data,
22 createdAt: new Date(),
23 });
24
25 await this.emailService.sendWelcome(user.email, user.name);
26
27 return user;
28 }
29
30 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}
36
37module.exports = UserService;
38
39// === tests/unit/userService.test.js ===
40const UserService = require("../../src/services/userService");
41
42describe("UserService", () => {
43 let userService;
44 let mockUserRepo;
45 let mockEmailService;
46
47 // Setup before each test
48 beforeEach(() => {
49 // Create mocks
50 mockUserRepo = {
51 findByEmail: jest.fn(),
52 findById: jest.fn(),
53 create: jest.fn(),
54 };
55 mockEmailService = {
56 sendWelcome: jest.fn().mockResolvedValue(true),
57 };
58
59 userService = new UserService(mockUserRepo, mockEmailService);
60 });
61
62 // Clear mocks after each test
63 afterEach(() => {
64 jest.clearAllMocks();
65 });
66
67 describe("createUser", () => {
68 const validUserData = { name: "Alice", email: "alice@example.com" };
69
70 test("should create a user successfully", async () => {
71 // Arrange
72 mockUserRepo.findByEmail.mockResolvedValue(null); // No existing user
73 mockUserRepo.create.mockResolvedValue({ id: 1, ...validUserData });
74
75 // Act
76 const user = await userService.createUser(validUserData);
77
78 // Assert
79 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 });
86
87 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 });
93
94 test("should throw if email already exists", async () => {
95 mockUserRepo.findByEmail.mockResolvedValue({ id: 1, email: validUserData.email });
96
97 await expect(userService.createUser(validUserData)).rejects.toThrow(
98 "Email already registered"
99 );
100 expect(mockUserRepo.create).not.toHaveBeenCalled();
101 });
102
103 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"));
107
108 await expect(userService.createUser(validUserData)).rejects.toThrow("SMTP error");
109 });
110 });
111
112 describe("getUserById", () => {
113 test("should return user when found", async () => {
114 const mockUser = { id: 1, name: "Alice" };
115 mockUserRepo.findById.mockResolvedValue(mockUser);
116
117 const user = await userService.getUserById(1);
118
119 expect(user).toEqual(mockUser);
120 expect(mockUserRepo.findById).toHaveBeenCalledWith(1);
121 });
122
123 test("should throw when user not found", async () => {
124 mockUserRepo.findById.mockResolvedValue(null);
125
126 await expect(userService.getUserById(999)).rejects.toThrow("User not found");
127 });
128 });
129});
130
131// === 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:

  1. Write unit tests for a Calculator class with add, subtract, multiply, divide — cover edge cases
  2. Test an async service with mocked dependencies — use jest.fn() for repository and external services
  3. Achieve 90%+ code coverage on a service module — use jest --coverage and fill gaps
  4. Write tests using beforeEach, afterEach, beforeAll, afterAll for setup and cleanup
  5. Test error handling — verify that functions throw specific errors with specific messages
  6. 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() in afterEach

  • Writing tests that depend on execution order — each test should be independent; use beforeEach for setup, not shared mutable state

  • Only 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.