Learn Jest unit testing in JavaScript from scratch. Setup, matchers, mocking, async testing, best practices with examples and interview questions.
Introduction
Unit testing is a practice where we independently test small parts (units) of our code. This means isolating a function or module and checking whether it produces the expected output for given inputs.
Why Unit Testing Matters
- Bugs are caught early — Errors are found before reaching production
- Refactoring becomes safe — After code changes, you have confidence that nothing broke
- Acts as documentation — Tests show what the code does
- Development speed increases — Debugging time decreases significantly in the long term
What is Jest?
Jest is a powerful JavaScript testing framework created by Facebook (Meta). It provides zero-configuration setup and works with React, Node.js, Vue, Angular — everything.
Jest ke features:
- Zero configuration — install and start writing tests
- Fast execution with parallel test running
- Built-in code coverage reports
- Powerful mocking capabilities
- Snapshot testing support
- Great error messages with diffs
Testing Pyramid
┌─────────────────────────────────────────────────────────┐
│ TESTING PYRAMID │
├─────────────────────────────────────────────────────────┤
│ │
│ /\ │
│ / \ │
│ / E2E\ ← Slow, Expensive │
│ /______\ (Selenium, Cypress) │
│ / \ │
│ /Integration\ ← Medium Speed │
│ /____________\ (API tests) │
│ / \ │
│ / UNIT TESTS \ ← Fast, Cheap │
│ / (Jest, Mocha) \ Most Tests Here! │
│ /____________________\ │
│ │
│ ✅ Unit Tests: 70-80% of all tests │
│ ✅ Integration Tests: 15-20% of all tests │
│ ✅ E2E Tests: 5-10% of all tests │
│ │
└─────────────────────────────────────────────────────────┘
Unit tests are at the bottom because they are written the most, run the fastest, and are the cheapest to maintain.
Setting Up Jest
Step 1: Initialize Project
mkdir jest-demo
cd jest-demo
npm init -y
Step 2: Install Jest
npm install --save-dev jest
{
"name": "jest-demo",
"version": "1.0.0",
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
},
"devDependencies": {
"jest": "^29.7.0"
}
}
Step 4: Recommended Folder Structure
project/
├── src/
│ ├── math.js
│ ├── userService.js
│ └── utils.js
├── tests/
│ ├── math.test.js
│ ├── userService.test.js
│ └── utils.test.js
├── package.json
└── jest.config.js (optional)
Optional: jest.config.js
module.exports = {
testEnvironment: 'node',
coverageDirectory: 'coverage',
collectCoverageFrom: ['src/**/*.js'],
testMatch: ['**/tests/**/*.test.js'],
verbose: true
};
Writing Your First Test
First, let's create a simple function and test it.
Source File: src/math.js
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
function multiply(a, b) {
return a * b;
}
module.exports = { add, subtract, multiply };
Test File: tests/math.test.js
const { add, subtract, multiply } = require('../src/math');
test('adds 2 + 3 to equal 5', () => {
expect(add(2, 3)).toBe(5);
});
test('subtracts 10 - 4 to equal 6', () => {
expect(subtract(10, 4)).toBe(6);
});
test('multiplies 3 * 7 to equal 21', () => {
expect(multiply(3, 7)).toBe(21);
});
PASS tests/math.test.js
✓ adds 2 + 3 to equal 5 (2 ms)
✓ subtracts 10 - 4 to equal 6 (1 ms)
✓ multiplies 3 * 7 to equal 21 (1 ms)
Test Suites: 1 passed, 1 total
Tests: 3 passed, 3 total
Snapshots: 0 total
Time: 0.534 s
Understanding the Syntax
test(name, fn) — Defines a test caseexpect(value) — Wraps a value for assertiontoBe(expected) — Strict equality check (===)
Jest Matchers
Jest has many matchers used for different types of checks.
Common Matchers Reference
// Exact equality
expect(2 + 2).toBe(4);
// Object equality (deep comparison)
expect({ name: 'Jest' }).toEqual({ name: 'Jest' });
// Truthiness
expect(true).toBeTruthy();
expect(false).toBeFalsy();
expect(null).toBeNull();
expect(undefined).toBeUndefined();
expect('hello').toBeDefined();
// Numbers
expect(10).toBeGreaterThan(5);
expect(5).toBeLessThan(10);
expect(10).toBeGreaterThanOrEqual(10);
expect(5.1 + 5.2).toBeCloseTo(10.3); // floating point
// Strings
expect('Hello World').toMatch(/World/);
expect('JavaScript').toContain('Script');
// Arrays
expect([1, 2, 3]).toContain(2);
expect(['apple', 'banana']).toHaveLength(2);
// Exceptions
expect(() => { throw new Error('fail'); }).toThrow();
expect(() => { throw new Error('fail'); }).toThrow('fail');
// Negation with .not
expect(5).not.toBe(3);
expect([1, 2, 3]).not.toContain(4);
PASS tests/matchers.test.js
✓ exact equality (1 ms)
✓ object equality (1 ms)
✓ truthiness checks (1 ms)
✓ number comparisons (1 ms)
✓ string matching (1 ms)
✓ array checks (1 ms)
✓ exception testing (1 ms)
✓ negation with .not (1 ms)
Test Suites: 1 passed, 1 total
Tests: 8 passed, 8 total
Time: 0.412 s
Code Examples
Example 1: Testing a String Utility Function
// src/stringUtils.js
function capitalize(str) {
if (typeof str !== 'string') throw new TypeError('Input must be a string');
if (str.length === 0) return '';
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
}
function reverseString(str) {
return str.split('').reverse().join('');
}
module.exports = { capitalize, reverseString };
// tests/stringUtils.test.js
const { capitalize, reverseString } = require('../src/stringUtils');
test('capitalizes first letter of a word', () => {
expect(capitalize('hello')).toBe('Hello');
});
test('handles already capitalized string', () => {
expect(capitalize('HELLO')).toBe('Hello');
});
test('handles empty string', () => {
expect(capitalize('')).toBe('');
});
test('throws error for non-string input', () => {
expect(() => capitalize(123)).toThrow(TypeError);
expect(() => capitalize(123)).toThrow('Input must be a string');
});
test('reverses a string correctly', () => {
expect(reverseString('hello')).toBe('olleh');
expect(reverseString('Jest')).toBe('tseJ');
});
PASS tests/stringUtils.test.js
✓ capitalizes first letter of a word (2 ms)
✓ handles already capitalized string (1 ms)
✓ handles empty string (1 ms)
✓ throws error for non-string input (1 ms)
✓ reverses a string correctly (1 ms)
Test Suites: 1 passed, 1 total
Tests: 5 passed, 5 total
Time: 0.389 s
Example 2: Testing Array/Object Functions
// src/arrayUtils.js
function findMax(arr) {
if (!Array.isArray(arr) || arr.length === 0) {
throw new Error('Input must be a non-empty array');
}
return Math.max(...arr);
}
function removeDuplicates(arr) {
return [...new Set(arr)];
}
function groupBy(arr, key) {
return arr.reduce((result, item) => {
const group = item[key];
result[group] = result[group] || [];
result[group].push(item);
return result;
}, {});
}
module.exports = { findMax, removeDuplicates, groupBy };
// tests/arrayUtils.test.js
const { findMax, removeDuplicates, groupBy } = require('../src/arrayUtils');
test('finds maximum value in array', () => {
expect(findMax([1, 5, 3, 9, 2])).toBe(9);
expect(findMax([-5, -1, -10])).toBe(-1);
});
test('throws error for empty array', () => {
expect(() => findMax([])).toThrow('Input must be a non-empty array');
});
test('removes duplicate values', () => {
expect(removeDuplicates([1, 2, 2, 3, 3, 3])).toEqual([1, 2, 3]);
expect(removeDuplicates(['a', 'b', 'a'])).toEqual(['a', 'b']);
});
test('groups objects by key', () => {
const people = [
{ name: 'Amit', city: 'Delhi' },
{ name: 'Priya', city: 'Mumbai' },
{ name: 'Rahul', city: 'Delhi' }
];
const result = groupBy(people, 'city');
expect(result).toEqual({
Delhi: [
{ name: 'Amit', city: 'Delhi' },
{ name: 'Rahul', city: 'Delhi' }
],
Mumbai: [
{ name: 'Priya', city: 'Mumbai' }
]
});
});
PASS tests/arrayUtils.test.js
✓ finds maximum value in array (2 ms)
✓ throws error for empty array (1 ms)
✓ removes duplicate values (1 ms)
✓ groups objects by key (1 ms)
Test Suites: 1 passed, 1 total
Tests: 4 passed, 4 total
Time: 0.356 s
Example 3: Testing Async Code with async/await
// src/userService.js
async function fetchUser(id) {
const response = await fetch(`https://api.example.com/users/${id}`);
if (!response.ok) {
throw new Error('User not found');
}
const data = await response.json();
return data;
}
async function fetchUserName(id) {
const user = await fetchUser(id);
return user.name;
}
module.exports = { fetchUser, fetchUserName };
// tests/userService.test.js
const { fetchUser, fetchUserName } = require('../src/userService');
// Mock the global fetch
global.fetch = jest.fn();
beforeEach(() => {
fetch.mockClear();
});
test('fetchUser returns user data on success', async () => {
const mockUser = { id: 1, name: 'Amit Kumar', email: 'amit@example.com' };
fetch.mockResolvedValueOnce({
ok: true,
json: async () => mockUser
});
const user = await fetchUser(1);
expect(user).toEqual(mockUser);
expect(fetch).toHaveBeenCalledWith('https://api.example.com/users/1');
});
test('fetchUser throws error when user not found', async () => {
fetch.mockResolvedValueOnce({
ok: false
});
await expect(fetchUser(999)).rejects.toThrow('User not found');
});
test('fetchUserName returns only the name', async () => {
fetch.mockResolvedValueOnce({
ok: true,
json: async () => ({ id: 1, name: 'Priya Sharma' })
});
const name = await fetchUserName(1);
expect(name).toBe('Priya Sharma');
});
PASS tests/userService.test.js
✓ fetchUser returns user data on success (3 ms)
✓ fetchUser throws error when user not found (1 ms)
✓ fetchUserName returns only the name (1 ms)
Test Suites: 1 passed, 1 total
Tests: 3 passed, 3 total
Time: 0.445 s
Example 4: Mock Functions with jest.fn()
// src/calculator.js
function calculate(a, b, operationFn) {
if (typeof operationFn !== 'function') {
throw new Error('Third argument must be a function');
}
return operationFn(a, b);
}
function processItems(items, callback) {
const results = [];
for (const item of items) {
results.push(callback(item));
}
return results;
}
module.exports = { calculate, processItems };
// tests/calculator.test.js
const { calculate, processItems } = require('../src/calculator');
test('calculate calls the operation function with correct args', () => {
const mockOperation = jest.fn((a, b) => a + b);
const result = calculate(5, 3, mockOperation);
expect(result).toBe(8);
expect(mockOperation).toHaveBeenCalledTimes(1);
expect(mockOperation).toHaveBeenCalledWith(5, 3);
});
test('processItems calls callback for each item', () => {
const mockCallback = jest.fn(x => x * 2);
const result = processItems([1, 2, 3], mockCallback);
expect(result).toEqual([2, 4, 6]);
expect(mockCallback).toHaveBeenCalledTimes(3);
expect(mockCallback).toHaveBeenNthCalledWith(1, 1);
expect(mockCallback).toHaveBeenNthCalledWith(2, 2);
expect(mockCallback).toHaveBeenNthCalledWith(3, 3);
});
test('mock function tracks return values', () => {
const mockFn = jest.fn()
.mockReturnValueOnce('first')
.mockReturnValueOnce('second')
.mockReturnValue('default');
expect(mockFn()).toBe('first');
expect(mockFn()).toBe('second');
expect(mockFn()).toBe('default');
expect(mockFn()).toBe('default');
});
PASS tests/calculator.test.js
✓ calculate calls the operation function with correct args (2 ms)
✓ processItems calls callback for each item (1 ms)
✓ mock function tracks return values (1 ms)
Test Suites: 1 passed, 1 total
Tests: 3 passed, 3 total
Time: 0.367 s
Example 5: Testing Error Handling
// src/validator.js
function validateEmail(email) {
if (!email) throw new Error('Email is required');
if (typeof email !== 'string') throw new TypeError('Email must be a string');
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
throw new Error('Invalid email format');
}
return true;
}
function validateAge(age) {
if (age === undefined || age === null) throw new Error('Age is required');
if (typeof age !== 'number') throw new TypeError('Age must be a number');
if (age < 0 || age > 150) throw new RangeError('Age must be between 0 and 150');
return true;
}
module.exports = { validateEmail, validateAge };
// tests/validator.test.js
const { validateEmail, validateAge } = require('../src/validator');
describe('validateEmail', () => {
test('returns true for valid email', () => {
expect(validateEmail('test@example.com')).toBe(true);
});
test('throws for empty email', () => {
expect(() => validateEmail('')).toThrow('Email is required');
expect(() => validateEmail(null)).toThrow('Email is required');
});
test('throws TypeError for non-string input', () => {
expect(() => validateEmail(123)).toThrow(TypeError);
});
test('throws for invalid email format', () => {
expect(() => validateEmail('invalid')).toThrow('Invalid email format');
expect(() => validateEmail('no@domain')).toThrow('Invalid email format');
});
});
describe('validateAge', () => {
test('returns true for valid age', () => {
expect(validateAge(25)).toBe(true);
expect(validateAge(0)).toBe(true);
expect(validateAge(150)).toBe(true);
});
test('throws for missing age', () => {
expect(() => validateAge(undefined)).toThrow('Age is required');
expect(() => validateAge(null)).toThrow('Age is required');
});
test('throws TypeError for non-number', () => {
expect(() => validateAge('25')).toThrow(TypeError);
});
test('throws RangeError for out-of-range age', () => {
expect(() => validateAge(-1)).toThrow(RangeError);
expect(() => validateAge(151)).toThrow(RangeError);
});
});
PASS tests/validator.test.js
validateEmail
✓ returns true for valid email (2 ms)
✓ throws for empty email (1 ms)
✓ throws TypeError for non-string input (1 ms)
✓ throws for invalid email format (1 ms)
validateAge
✓ returns true for valid age (1 ms)
✓ throws for missing age (1 ms)
✓ throws TypeError for non-number (1 ms)
✓ throws RangeError for out-of-range age (1 ms)
Test Suites: 1 passed, 1 total
Tests: 8 passed, 8 total
Time: 0.423 sExample 6: beforeEach and afterEach Hooks
// src/shoppingCart.js
class ShoppingCart {
constructor() {
this.items = [];
}
addItem(name, price, quantity = 1) {
this.items.push({ name, price, quantity });
}
removeItem(name) {
this.items = this.items.filter(item => item.name !== name);
}
getTotal() {
return this.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
getItemCount() {
return this.items.reduce((count, item) => count + item.quantity, 0);
}
clear() {
this.items = [];
}
}
module.exports = ShoppingCart;
// tests/shoppingCart.test.js
const ShoppingCart = require('../src/shoppingCart');
describe('ShoppingCart', () => {
let cart;
beforeEach(() => {
// Fresh cart before each test — isolation!
cart = new ShoppingCart();
});
afterEach(() => {
// Cleanup after each test
cart.clear();
});
test('starts with empty cart', () => {
expect(cart.getItemCount()).toBe(0);
expect(cart.getTotal()).toBe(0);
});
test('adds items correctly', () => {
cart.addItem('Laptop', 50000);
cart.addItem('Mouse', 500, 2);
expect(cart.getItemCount()).toBe(3);
expect(cart.getTotal()).toBe(51000);
});
test('removes item by name', () => {
cart.addItem('Phone', 20000);
cart.addItem('Case', 500);
cart.removeItem('Phone');
expect(cart.getItemCount()).toBe(1);
expect(cart.getTotal()).toBe(500);
});
test('calculates total with quantities', () => {
cart.addItem('Book', 300, 3);
cart.addItem('Pen', 50, 5);
expect(cart.getTotal()).toBe(1150);
});
test('clear empties the cart', () => {
cart.addItem('Item1', 100);
cart.addItem('Item2', 200);
cart.clear();
expect(cart.getItemCount()).toBe(0);
expect(cart.getTotal()).toBe(0);
});
});
PASS tests/shoppingCart.test.js
ShoppingCart
✓ starts with empty cart (2 ms)
✓ adds items correctly (1 ms)
✓ removes item by name (1 ms)
✓ calculates total with quantities (1 ms)
✓ clear empties the cart (1 ms)
Test Suites: 1 passed, 1 total
Tests: 5 passed, 5 total
Time: 0.398 sExample 7: Describe Blocks for Organization
// src/passwordValidator.js
function validatePassword(password) {
const errors = [];
if (password.length < 8) errors.push('Must be at least 8 characters');
if (!/[A-Z]/.test(password)) errors.push('Must contain uppercase letter');
if (!/[a-z]/.test(password)) errors.push('Must contain lowercase letter');
if (!/[0-9]/.test(password)) errors.push('Must contain a number');
if (!/[!@#$%^&*]/.test(password)) errors.push('Must contain special character');
return {
isValid: errors.length === 0,
errors
};
}
module.exports = { validatePassword };
// tests/passwordValidator.test.js
const { validatePassword } = require('../src/passwordValidator');
describe('Password Validator', () => {
describe('valid passwords', () => {
test('accepts strong password', () => {
const result = validatePassword('MyP@ss123');
expect(result.isValid).toBe(true);
expect(result.errors).toHaveLength(0);
});
test('accepts password with all requirements', () => {
const result = validatePassword('Str0ng!Pass');
expect(result.isValid).toBe(true);
});
});
describe('invalid passwords', () => {
test('rejects short password', () => {
const result = validatePassword('Ab1!');
expect(result.isValid).toBe(false);
expect(result.errors).toContain('Must be at least 8 characters');
});
test('rejects password without uppercase', () => {
const result = validatePassword('lowercase1!');
expect(result.isValid).toBe(false);
expect(result.errors).toContain('Must contain uppercase letter');
});
test('rejects password without number', () => {
const result = validatePassword('NoNumber!Here');
expect(result.isValid).toBe(false);
expect(result.errors).toContain('Must contain a number');
});
test('rejects password without special character', () => {
const result = validatePassword('NoSpecial1Here');
expect(result.isValid).toBe(false);
expect(result.errors).toContain('Must contain special character');
});
});
describe('edge cases', () => {
test('empty password fails all rules', () => {
const result = validatePassword('');
expect(result.isValid).toBe(false);
expect(result.errors.length).toBeGreaterThanOrEqual(4);
});
});
});
PASS tests/passwordValidator.test.js
Password Validator
valid passwords
✓ accepts strong password (2 ms)
✓ accepts password with all requirements (1 ms)
invalid passwords
✓ rejects short password (1 ms)
✓ rejects password without uppercase (1 ms)
✓ rejects password without number (1 ms)
✓ rejects password without special character (1 ms)
edge cases
✓ empty password fails all rules (1 ms)
Test Suites: 1 passed, 1 total
Tests: 7 passed, 7 total
Time: 0.401 sExample 8: Snapshot Testing Concept
// src/formatUser.js
function formatUserProfile(user) {
return {
displayName: `${user.firstName} ${user.lastName}`,
email: user.email.toLowerCase(),
avatar: `https://avatars.example.com/${user.id}.png`,
memberSince: new Date(user.joinDate).toLocaleDateString('en-IN'),
role: user.isAdmin ? 'Administrator' : 'Member'
};
}
module.exports = { formatUserProfile };
// tests/formatUser.test.js
const { formatUserProfile } = require('../src/formatUser');
describe('formatUserProfile', () => {
const mockUser = {
id: 42,
firstName: 'Rahul',
lastName: 'Verma',
email: 'RAHUL@Example.COM',
joinDate: '2024-01-15',
isAdmin: false
};
test('formats user profile correctly (snapshot)', () => {
const profile = formatUserProfile(mockUser);
// Snapshot test — first run creates snapshot file
// Subsequent runs compare against saved snapshot
expect(profile).toMatchSnapshot();
});
test('formats admin user correctly', () => {
const adminUser = { ...mockUser, isAdmin: true };
const profile = formatUserProfile(adminUser);
expect(profile).toMatchInlineSnapshot(`
{
"avatar": "https://avatars.example.com/42.png",
"displayName": "Rahul Verma",
"email": "rahul@example.com",
"memberSince": "15/1/2024",
"role": "Administrator",
}
`);
});
test('lowercases email', () => {
const profile = formatUserProfile(mockUser);
expect(profile.email).toBe('rahul@example.com');
});
});
PASS tests/formatUser.test.js
formatUserProfile
✓ formats user profile correctly (snapshot) (4 ms)
✓ formats admin user correctly (2 ms)
✓ lowercases email (1 ms)
› 1 snapshot written.
Test Suites: 1 passed, 1 total
Tests: 3 passed, 3 total
Snapshots: 1 written, 1 passed, 2 total
Time: 0.512 sMocking
Mocking means replacing real dependencies with fake versions. This is useful when:
- There are external API calls
- There are database operations
- There are file system operations
- Time-dependent code hai
jest.fn() — Creating Mock Functions
// Simple mock function
const mockFn = jest.fn();
mockFn('hello');
mockFn('world');
console.log(mockFn.mock.calls); // [['hello'], ['world']]
console.log(mockFn.mock.calls.length); // 2
// Mock with return value
const mockAdd = jest.fn().mockReturnValue(42);
console.log(mockAdd(1, 2)); // 42
// Mock with implementation
const mockMultiply = jest.fn((a, b) => a * b);
console.log(mockMultiply(3, 4)); // 12
jest.mock() — Mocking Modules
// src/emailService.js
const nodemailer = require('nodemailer');
async function sendWelcomeEmail(userEmail, userName) {
const transporter = nodemailer.createTransport({ /* config */ });
const info = await transporter.sendMail({
from: 'noreply@example.com',
to: userEmail,
subject: `Welcome ${userName}!`,
text: `Hello ${userName}, welcome to our platform!`
});
return info.messageId;
}
module.exports = { sendWelcomeEmail };
// tests/emailService.test.js
const { sendWelcomeEmail } = require('../src/emailService');
const nodemailer = require('nodemailer');
// Mock the entire nodemailer module
jest.mock('nodemailer');
describe('Email Service', () => {
test('sends welcome email with correct params', async () => {
const mockSendMail = jest.fn().mockResolvedValue({
messageId: 'msg-123'
});
nodemailer.createTransport.mockReturnValue({
sendMail: mockSendMail
});
const result = await sendWelcomeEmail('amit@test.com', 'Amit');
expect(result).toBe('msg-123');
expect(mockSendMail).toHaveBeenCalledWith({
from: 'noreply@example.com',
to: 'amit@test.com',
subject: 'Welcome Amit!',
text: 'Hello Amit, welcome to our platform!'
});
});
test('handles email sending failure', async () => {
const mockSendMail = jest.fn().mockRejectedValue(
new Error('SMTP connection failed')
);
nodemailer.createTransport.mockReturnValue({
sendMail: mockSendMail
});
await expect(
sendWelcomeEmail('test@test.com', 'Test')
).rejects.toThrow('SMTP connection failed');
});
});
PASS tests/emailService.test.js
Email Service
✓ sends welcome email with correct params (3 ms)
✓ handles email sending failure (1 ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Time: 0.478 sMocking Timer Functions
// src/debounce.js
function debounce(fn, delay) {
let timeoutId;
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn(...args), delay);
};
}
module.exports = { debounce };
// tests/debounce.test.js
const { debounce } = require('../src/debounce');
jest.useFakeTimers();
test('debounce delays function execution', () => {
const mockFn = jest.fn();
const debouncedFn = debounce(mockFn, 500);
debouncedFn('hello');
// Function not called yet
expect(mockFn).not.toHaveBeenCalled();
// Fast-forward time by 500ms
jest.advanceTimersByTime(500);
// Now it should be called
expect(mockFn).toHaveBeenCalledWith('hello');
expect(mockFn).toHaveBeenCalledTimes(1);
});
test('debounce resets timer on repeated calls', () => {
const mockFn = jest.fn();
const debouncedFn = debounce(mockFn, 300);
debouncedFn('first');
jest.advanceTimersByTime(200);
debouncedFn('second');
jest.advanceTimersByTime(200);
debouncedFn('third');
jest.advanceTimersByTime(300);
// Only the last call should execute
expect(mockFn).toHaveBeenCalledTimes(1);
expect(mockFn).toHaveBeenCalledWith('third');
});
PASS tests/debounce.test.js
✓ debounce delays function execution (2 ms)
✓ debounce resets timer on repeated calls (1 ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Time: 0.345 s
Testing Async Code
Async code is very common in JavaScript. Jest provides multiple approaches for async testing.
Approach 1: async/await
// src/api.js
async function getPost(id) {
const response = await fetch(`https://api.example.com/posts/${id}`);
if (!response.ok) throw new Error('Post not found');
return response.json();
}
async function createPost(title, body) {
const response = await fetch('https://api.example.com/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, body })
});
return response.json();
}
module.exports = { getPost, createPost };
// tests/api.test.js
const { getPost, createPost } = require('../src/api');
global.fetch = jest.fn();
afterEach(() => {
jest.resetAllMocks();
});
// async/await approach
test('getPost fetches and returns post data', async () => {
const mockPost = { id: 1, title: 'Jest Guide', body: 'Testing is fun' };
fetch.mockResolvedValue({
ok: true,
json: async () => mockPost
});
const post = await getPost(1);
expect(post).toEqual(mockPost);
expect(fetch).toHaveBeenCalledWith('https://api.example.com/posts/1');
});
// Testing rejections
test('getPost throws for non-existent post', async () => {
fetch.mockResolvedValue({ ok: false });
await expect(getPost(999)).rejects.toThrow('Post not found');
});
// Approach 2: resolves/rejects matchers
test('createPost resolves with new post', async () => {
const newPost = { id: 10, title: 'New Post', body: 'Content' };
fetch.mockResolvedValue({
json: async () => newPost
});
await expect(createPost('New Post', 'Content')).resolves.toEqual(newPost);
});
PASS tests/api.test.js
✓ getPost fetches and returns post data (3 ms)
✓ getPost throws for non-existent post (1 ms)
✓ createPost resolves with new post (1 ms)
Test Suites: 1 passed, 1 total
Tests: 3 passed, 3 total
Time: 0.412 s
Approach 2: Testing Promises with .resolves / .rejects
// src/promiseUtils.js
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function fetchWithTimeout(url, timeout = 5000) {
return Promise.race([
fetch(url).then(r => r.json()),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Request timeout')), timeout)
)
]);
}
module.exports = { delay, fetchWithTimeout };
// tests/promiseUtils.test.js
const { delay, fetchWithTimeout } = require('../src/promiseUtils');
jest.useFakeTimers();
test('delay resolves after specified time', async () => {
const promise = delay(1000);
jest.advanceTimersByTime(1000);
await expect(promise).resolves.toBeUndefined();
});
test('fetchWithTimeout rejects on timeout', async () => {
global.fetch = jest.fn(() => new Promise(() => {})); // never resolves
const promise = fetchWithTimeout('https://slow-api.com', 3000);
jest.advanceTimersByTime(3000);
await expect(promise).rejects.toThrow('Request timeout');
});
PASS tests/promiseUtils.test.js
✓ delay resolves after specified time (2 ms)
✓ fetchWithTimeout rejects on timeout (1 ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Time: 0.378 s
Test Organization
Complete Lifecycle Hooks Example
// tests/database.test.js
const Database = require('../src/database');
describe('Database Operations', () => {
let db;
// Runs ONCE before all tests in this describe block
beforeAll(async () => {
db = new Database();
await db.connect();
console.log('Database connected');
});
// Runs ONCE after all tests in this describe block
afterAll(async () => {
await db.disconnect();
console.log('Database disconnected');
});
// Runs before EACH test
beforeEach(async () => {
await db.clear();
await db.seed([
{ id: 1, name: 'Amit', role: 'developer' },
{ id: 2, name: 'Priya', role: 'designer' }
]);
});
// Runs after EACH test
afterEach(() => {
jest.restoreAllMocks();
});
describe('Read Operations', () => {
test('findAll returns all records', async () => {
const users = await db.findAll();
expect(users).toHaveLength(2);
});
test('findById returns specific record', async () => {
const user = await db.findById(1);
expect(user.name).toBe('Amit');
});
test('findById returns null for missing record', async () => {
const user = await db.findById(999);
expect(user).toBeNull();
});
});
describe('Write Operations', () => {
test('insert adds new record', async () => {
await db.insert({ id: 3, name: 'Rahul', role: 'tester' });
const users = await db.findAll();
expect(users).toHaveLength(3);
});
test('delete removes record', async () => {
await db.delete(1);
const users = await db.findAll();
expect(users).toHaveLength(1);
expect(users[0].name).toBe('Priya');
});
});
});
PASS tests/database.test.js
Database Operations
Read Operations
✓ findAll returns all records (3 ms)
✓ findById returns specific record (1 ms)
✓ findById returns null for missing record (1 ms)
Write Operations
✓ insert adds new record (2 ms)
✓ delete removes record (1 ms)
console.log
Database connected
Database disconnected
Test Suites: 1 passed, 1 total
Tests: 5 passed, 5 total
Time: 0.567 sExecution Order of Hooks
┌────────────────────────────────────────────────┐
│ JEST HOOKS EXECUTION ORDER │
├────────────────────────────────────────────────┤
│ │
│ beforeAll() ← Once before ALL tests │
│ │ │
│ ├── beforeEach() ← Before EACH test │
│ │ └── test 1 │
│ │ afterEach() ← After EACH test │
│ │ │
│ ├── beforeEach() │
│ │ └── test 2 │
│ │ afterEach() │
│ │ │
│ ├── beforeEach() │
│ │ └── test 3 │
│ │ afterEach() │
│ │ │
│ afterAll() ← Once after ALL tests │
│ │
└────────────────────────────────────────────────┘
🚫 Common Mistakes
Mistake 1: Not Returning Promises in Async Tests
// ❌ WRONG — Test passes even if promise rejects!
test('fetches data', () => {
fetchData().then(data => {
expect(data).toBe('expected');
});
// Test ends before promise resolves!
});
// ✅ CORRECT — Use async/await
test('fetches data', async () => {
const data = await fetchData();
expect(data).toBe('expected');
});
// ✅ ALSO CORRECT — Return the promise
test('fetches data', () => {
return fetchData().then(data => {
expect(data).toBe('expected');
});
});
Mistake 2: Shared Mutable State Between Tests
// ❌ WRONG — Tests depend on each other
let counter = 0;
test('increments counter', () => {
counter++;
expect(counter).toBe(1);
});
test('counter is still 1', () => {
// This DEPENDS on previous test running first!
expect(counter).toBe(1);
});
// ✅ CORRECT — Reset state in beforeEach
describe('Counter tests', () => {
let counter;
beforeEach(() => {
counter = 0; // Fresh state for each test
});
test('increments counter', () => {
counter++;
expect(counter).toBe(1);
});
test('starts from zero', () => {
expect(counter).toBe(0);
});
});
Mistake 3: Testing Implementation Instead of Behavior
// ❌ WRONG — Testing HOW it works (implementation)
test('uses Array.reduce internally', () => {
const spy = jest.spyOn(Array.prototype, 'reduce');
calculateTotal([1, 2, 3]);
expect(spy).toHaveBeenCalled();
});
// ✅ CORRECT — Testing WHAT it does (behavior)
test('calculates sum of array', () => {
expect(calculateTotal([1, 2, 3])).toBe(6);
expect(calculateTotal([10, 20])).toBe(30);
expect(calculateTotal([])).toBe(0);
});
Mistake 4: Forgetting to Clear Mocks
// ❌ WRONG — Mock state leaks between tests
const mockFn = jest.fn();
test('first test', () => {
mockFn('hello');
expect(mockFn).toHaveBeenCalledTimes(1);
});
test('second test', () => {
mockFn('world');
// FAILS! mockFn was called 2 times total (leak from first test)
expect(mockFn).toHaveBeenCalledTimes(1);
});
// ✅ CORRECT — Clear mocks between tests
describe('with mock cleanup', () => {
const mockFn = jest.fn();
afterEach(() => {
mockFn.mockClear(); // or jest.clearAllMocks()
});
test('first test', () => {
mockFn('hello');
expect(mockFn).toHaveBeenCalledTimes(1);
});
test('second test', () => {
mockFn('world');
expect(mockFn).toHaveBeenCalledTimes(1); // ✓ Passes!
});
});
Mistake 5: Using toBe() for Object Comparison
// ❌ WRONG — toBe uses === (reference equality)
test('objects are equal', () => {
const result = getUser();
expect(result).toBe({ name: 'Amit', age: 25 });
// FAILS! Two different object references
});
// ✅ CORRECT — Use toEqual for deep equality
test('objects are equal', () => {
const result = getUser();
expect(result).toEqual({ name: 'Amit', age: 25 });
// PASSES! Checks values recursively
});
// ✅ ALSO USEFUL — toMatchObject for partial matching
test('object contains expected fields', () => {
const result = getUser(); // might have more fields
expect(result).toMatchObject({ name: 'Amit' });
});
Mistake 6: Not Handling Async Errors Properly
// ❌ WRONG — This does NOT test the rejection
test('rejects with error', () => {
expect(failingAsyncFn()).rejects.toThrow('error');
// Missing await! Test passes without actually checking
});
// ✅ CORRECT — Always await async assertions
test('rejects with error', async () => {
await expect(failingAsyncFn()).rejects.toThrow('error');
});
🎯 Key Takeaways
- Unit tests should be fast — A single test should not take more than 50ms. Slow tests degrade the developer experience.
- Each test should be independent — One test should not depend on another. Set up fresh state in beforeEach.
- Test behavior, not implementation — Test WHAT a function does, not HOW it does it. Tests shouldn't break when implementation changes.
- Follow the AAA pattern — Arrange (setup), Act (execute), Assert (verify). This pattern helps write readable tests.
- Use mocks sparingly — Only mock external dependencies (APIs, databases, file system). Don't unnecessarily mock internal functions.
- Test edge cases — Cover empty inputs, null values, boundary conditions, and error cases.
- Write descriptive test names — The test name should convey what's being tested without needing to look at the code.
- Don't target 100% code coverage — 80-90% coverage is realistic. Quality > Quantity. There's no need to test trivial getters/setters.
- Mock cleanup is important — Clear mocks in afterEach, otherwise state will leak and you'll get flaky tests.
- Tests are code too — Apply the DRY principle, create helper functions, but don't sacrifice readability.
❓ Interview Questions
Q1: What is the difference between Jest and Mocha?
Answer: Jest is a complete testing framework that provides an out-of-the-box assertion library, mocking, code coverage, and snapshot testing. Mocha is only a test runner — you need separate libraries for assertions (Chai), mocking (Sinon), and coverage (Istanbul). Jest is recommended for most JavaScript projects due to its zero-configuration approach.
Q2: What is the difference between jest.fn() and jest.spyOn()?
Answer: jest.fn() creates a new mock function from scratch — it doesn't replace any existing function. jest.spyOn() watches an existing object's method — the original implementation can still be called unless you override it. Use jest.fn() for callbacks and jest.spyOn() when you want to track calls to existing methods.
// jest.fn() — brand new function
const mockFn = jest.fn().mockReturnValue(42);
// jest.spyOn() — watches existing method
const spy = jest.spyOn(Math, 'random').mockReturnValue(0.5);
// Later: spy.mockRestore() brings back real Math.random
Q3: When should snapshot testing be used?
Answer: Snapshot testing should be used when output is large and predictable — like React component render output, formatted data structures, and API response shapes. It's useful for regression testing but should be updated carefully when snapshots change.
Q4: What is the difference between beforeAll and beforeEach?
Answer: beforeAll runs only once — before all tests in a describe block. Use it for heavy setup like database connections. beforeEach runs before each test — use it for fresh state setup so tests don't affect each other.
Q5: What are flaky tests and how do you fix them?
Answer: Flaky tests are those that sometimes pass, sometimes fail — random behavior. Common causes:
- Shared mutable state between tests
- Race conditions in async code
- Time-dependent tests without fake timers
- Order-dependent tests
- Network dependency in unit tests
Fix: Ensure test isolation (beforeEach reset), use fake timers, mock external calls, debug order with --runInBand.
Q6: What does code coverage mean and how much coverage is needed?
Answer: Code coverage shows how much code the tests executed — lines, branches, functions, statements. Use the --coverage flag in Jest. 80-90% coverage is good. 100% is not practical and often leads to testing trivial code.
Q7: What is Test Driven Development (TDD)?
Answer: In TDD, you write tests first, then write code that passes the test, then refactor. The cycle: Red (failing test) → Green (make it pass) → Refactor (clean up). Benefits: better design, fewer bugs, built-in documentation.
📝 FAQ
Q: Is Jest only for React?
No! Jest originally came with React, but it's a general-purpose JavaScript testing framework. It works with Node.js backend, vanilla JavaScript, Vue, Angular, TypeScript — everything.
Q: How do you run a specific test file or test in Jest?
# Run a specific file
npx jest tests/math.test.js
# Match a specific test name
npx jest --testNamePattern="adds 2 + 3"
# Specific test inside file with .only
test.only('this will run', () => {
expect(true).toBe(true);
});
# In watch mode press 'p' to filter files
npx jest --watch
Q: Why must jest.mock() be written at the top of the module?
Jest internally hoists jest.mock() calls to the top of the file (even before imports). This is necessary because the mock must be in place before the module loads. If you need dynamic mock values, use jest.mock() with a factory function.
Q: What is the "Arrange, Act, Assert" pattern in testing?
test('user registration', async () => {
// ARRANGE — Setup test data and dependencies
const userData = { name: 'Amit', email: 'amit@test.com' };
const mockDb = { save: jest.fn().mockResolvedValue({ id: 1 }) };
// ACT — Execute the function being tested
const result = await registerUser(userData, mockDb);
// ASSERT — Verify the outcome
expect(result.id).toBe(1);
expect(mockDb.save).toHaveBeenCalledWith(userData);
});
This pattern makes tests readable and maintainable. Each test is clearly divided into three sections.
Q: How do you use TypeScript with Jest?
# Install dependencies
npm install --save-dev jest ts-jest @types/jest typescript
# Create jest.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};
The ts-jest preset automatically compiles TypeScript files during the test run. Alternatively, you can use @swc/jest for faster compilation. Installing @types/jest provides IDE autocomplete for Jest APIs.
*After mastering Jest, you can confidently write production code knowing that your tests act as a safety net. Happy Testing! 🧪*