Jest Testing
Learn Jest testing framework for testing React components and JavaScript code
80 minโขBy Priygop TeamโขLast updated: Feb 2026
What is Jest?
Jest is a JavaScript testing framework designed to ensure correctness of any JavaScript codebase. It allows you to write tests with an approachable, familiar and feature-rich API that gives you results quickly.
Basic Jest Tests
Example
// Basic Jest test
function sum(a, b) {
return a + b;
}
// Test file: sum.test.js
describe('sum function', () => {
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});
test('adds negative numbers correctly', () => {
expect(sum(-1, -2)).toBe(-3);
});
test('handles zero correctly', () => {
expect(sum(0, 5)).toBe(5);
});
});
// Testing async functions
async function fetchUser(id) {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
describe('fetchUser', () => {
test('fetches user successfully', async () => {
const user = await fetchUser(1);
expect(user).toHaveProperty('id');
expect(user).toHaveProperty('name');
});
test('handles errors correctly', async () => {
await expect(fetchUser(999)).rejects.toThrow();
});
});
// Mocking functions
const mockFetch = jest.fn();
beforeEach(() => {
mockFetch.mockClear();
});
test('mocks fetch correctly', async () => {
mockFetch.mockResolvedValueOnce({
json: async () => ({ id: 1, name: 'John' })
});
const result = await fetchUser(1);
expect(result).toEqual({ id: 1, name: 'John' });
expect(mockFetch).toHaveBeenCalledWith('/api/users/1');
});
// Testing utility functions
// src/utils/validation.js
export function validateEmail(email) {
const emailRegex = /^[^s@]+@[^s@]+.[^s@]+$/;
return emailRegex.test(email);
}
export function validatePassword(password) {
return password.length >= 8 &&
/[A-Z]/.test(password) &&
/[a-z]/.test(password) &&
/[0-9]/.test(password);
}
// src/utils/validation.test.js
import { validateEmail, validatePassword } from './validation';
describe('validateEmail', () => {
test('validates correct email formats', () => {
expect(validateEmail('test@example.com')).toBe(true);
expect(validateEmail('user.name@domain.co.uk')).toBe(true);
expect(validateEmail('user+tag@example.org')).toBe(true);
});
test('rejects invalid email formats', () => {
expect(validateEmail('invalid-email')).toBe(false);
expect(validateEmail('test@')).toBe(false);
expect(validateEmail('@example.com')).toBe(false);
expect(validateEmail('')).toBe(false);
});
});
describe('validatePassword', () => {
test('validates strong passwords', () => {
expect(validatePassword('StrongPass123')).toBe(true);
expect(validatePassword('MySecure1')).toBe(true);
});
test('rejects weak passwords', () => {
expect(validatePassword('weak')).toBe(false);
expect(validatePassword('12345678')).toBe(false);
expect(validatePassword('onlylowercase')).toBe(false);
expect(validatePassword('ONLYUPPERCASE')).toBe(false);
});
});
// Testing API utilities
// src/utils/api.js
export class ApiClient {
constructor(baseURL) {
this.baseURL = baseURL;
}
async get(endpoint) {
const response = await fetch(`${this.baseURL}${endpoint}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}
async post(endpoint, data) {
const response = await fetch(`${this.baseURL}${endpoint}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}
}
// src/utils/api.test.js
import { ApiClient } from './api';
describe('ApiClient', () => {
let apiClient;
let mockFetch;
beforeEach(() => {
apiClient = new ApiClient('https://api.example.com');
mockFetch = jest.fn();
global.fetch = mockFetch;
});
afterEach(() => {
jest.restoreAllMocks();
});
describe('get', () => {
test('successfully fetches data', async () => {
const mockData = { id: 1, name: 'John' };
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockData,
});
const result = await apiClient.get('/users/1');
expect(result).toEqual(mockData);
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/users/1');
});
test('throws error on failed request', async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 404,
});
await expect(apiClient.get('/users/999')).rejects.toThrow('HTTP error! status: 404');
});
});
describe('post', () => {
test('successfully posts data', async () => {
const postData = { name: 'John', email: 'john@example.com' };
const mockResponse = { id: 1, ...postData };
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockResponse,
});
const result = await apiClient.post('/users', postData);
expect(result).toEqual(mockResponse);
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(postData),
});
});
});
});Practice Exercise: Complete Testing Suite
Example
// Exercise: Build a Complete Testing Suite for a Todo App
// Create comprehensive tests for a React Todo application
// src/components/TodoApp.js
import React, { useState, useEffect } from 'react';
function TodoApp() {
const [todos, setTodos] = useState([]);
const [newTodo, setNewTodo] = useState('');
const [filter, setFilter] = useState('all');
useEffect(() => {
const savedTodos = localStorage.getItem('todos');
if (savedTodos) {
setTodos(JSON.parse(savedTodos));
}
}, []);
useEffect(() => {
localStorage.setItem('todos', JSON.stringify(todos));
}, [todos]);
const addTodo = () => {
if (newTodo.trim()) {
setTodos(prev => [...prev, {
id: Date.now(),
text: newTodo,
completed: false,
createdAt: new Date().toISOString()
}]);
setNewTodo('');
}
};
const toggleTodo = (id) => {
setTodos(prev => prev.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
};
const deleteTodo = (id) => {
setTodos(prev => prev.filter(todo => todo.id !== id));
};
const filteredTodos = todos.filter(todo => {
if (filter === 'active') return !todo.completed;
if (filter === 'completed') return todo.completed;
return true;
});
return (
<div className="todo-app">
<h1>Todo App</h1>
<div className="add-todo">
<input
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
placeholder="Add new todo..."
data-testid="new-todo-input"
/>
<button onClick={addTodo} data-testid="add-todo-button">
Add Todo
</button>
</div>
<div className="filters">
<button
onClick={() => setFilter('all')}
className={filter === 'all' ? 'active' : ''}
data-testid="filter-all"
>
All
</button>
<button
onClick={() => setFilter('active')}
className={filter === 'active' ? 'active' : ''}
data-testid="filter-active"
>
Active
</button>
<button
onClick={() => setFilter('completed')}
className={filter === 'completed' ? 'active' : ''}
data-testid="filter-completed"
>
Completed
</button>
</div>
<ul className="todo-list" data-testid="todo-list">
{filteredTodos.map(todo => (
<li key={todo.id} className="todo-item">
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
data-testid={`todo-checkbox-${todo.id}`}
/>
<span
className={todo.completed ? 'completed' : ''}
data-testid={`todo-text-${todo.id}`}
>
{todo.text}
</span>
<button
onClick={() => deleteTodo(todo.id)}
data-testid={`delete-todo-${todo.id}`}
>
Delete
</button>
</li>
))}
</ul>
<div className="todo-stats">
<span data-testid="todo-count">
{todos.filter(todo => !todo.completed).length} items left
</span>
</div>
</div>
);
}
// src/components/TodoApp.test.js
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom';
import TodoApp from './TodoApp';
// Mock localStorage
const localStorageMock = {
getItem: jest.fn(),
setItem: jest.fn(),
clear: jest.fn(),
};
global.localStorage = localStorageMock;
describe('TodoApp', () => {
beforeEach(() => {
localStorageMock.getItem.mockClear();
localStorageMock.setItem.mockClear();
});
test('renders todo app with title', () => {
render(<TodoApp />);
expect(screen.getByText('Todo App')).toBeInTheDocument();
});
test('adds new todo when form is submitted', () => {
render(<TodoApp />);
const input = screen.getByTestId('new-todo-input');
const addButton = screen.getByTestId('add-todo-button');
fireEvent.change(input, { target: { value: 'Test todo' } });
fireEvent.click(addButton);
expect(screen.getByText('Test todo')).toBeInTheDocument();
expect(input).toHaveValue('');
});
test('does not add empty todo', () => {
render(<TodoApp />);
const addButton = screen.getByTestId('add-todo-button');
fireEvent.click(addButton);
const todoList = screen.getByTestId('todo-list');
expect(todoList.children).toHaveLength(0);
});
test('toggles todo completion', () => {
render(<TodoApp />);
// Add a todo first
const input = screen.getByTestId('new-todo-input');
const addButton = screen.getByTestId('add-todo-button');
fireEvent.change(input, { target: { value: 'Test todo' } });
fireEvent.click(addButton);
const checkbox = screen.getByTestId('todo-checkbox-1');
const todoText = screen.getByTestId('todo-text-1');
expect(checkbox).not.toBeChecked();
expect(todoText).not.toHaveClass('completed');
fireEvent.click(checkbox);
expect(checkbox).toBeChecked();
expect(todoText).toHaveClass('completed');
});
test('deletes todo when delete button is clicked', () => {
render(<TodoApp />);
// Add a todo first
const input = screen.getByTestId('new-todo-input');
const addButton = screen.getByTestId('add-todo-button');
fireEvent.change(input, { target: { value: 'Test todo' } });
fireEvent.click(addButton);
expect(screen.getByText('Test todo')).toBeInTheDocument();
const deleteButton = screen.getByTestId('delete-todo-1');
fireEvent.click(deleteButton);
expect(screen.queryByText('Test todo')).not.toBeInTheDocument();
});
test('filters todos correctly', () => {
render(<TodoApp />);
// Add multiple todos
const input = screen.getByTestId('new-todo-input');
const addButton = screen.getByTestId('add-todo-button');
fireEvent.change(input, { target: { value: 'Todo 1' } });
fireEvent.click(addButton);
fireEvent.change(input, { target: { value: 'Todo 2' } });
fireEvent.click(addButton);
// Complete first todo
const checkbox1 = screen.getByTestId('todo-checkbox-1');
fireEvent.click(checkbox1);
// Test active filter
const activeFilter = screen.getByTestId('filter-active');
fireEvent.click(activeFilter);
expect(screen.getByText('Todo 2')).toBeInTheDocument();
expect(screen.queryByText('Todo 1')).not.toBeInTheDocument();
// Test completed filter
const completedFilter = screen.getByTestId('filter-completed');
fireEvent.click(completedFilter);
expect(screen.getByText('Todo 1')).toBeInTheDocument();
expect(screen.queryByText('Todo 2')).not.toBeInTheDocument();
});
test('loads todos from localStorage on mount', () => {
const savedTodos = [
{ id: 1, text: 'Saved todo', completed: false, createdAt: '2025-01-01' }
];
localStorageMock.getItem.mockReturnValue(JSON.stringify(savedTodos));
render(<TodoApp />);
expect(screen.getByText('Saved todo')).toBeInTheDocument();
expect(localStorageMock.getItem).toHaveBeenCalledWith('todos');
});
test('saves todos to localStorage when todos change', async () => {
render(<TodoApp />);
const input = screen.getByTestId('new-todo-input');
const addButton = screen.getByTestId('add-todo-button');
fireEvent.change(input, { target: { value: 'Test todo' } });
fireEvent.click(addButton);
await waitFor(() => {
expect(localStorageMock.setItem).toHaveBeenCalled();
});
});
test('displays correct todo count', () => {
render(<TodoApp />);
// Add two todos
const input = screen.getByTestId('new-todo-input');
const addButton = screen.getByTestId('add-todo-button');
fireEvent.change(input, { target: { value: 'Todo 1' } });
fireEvent.click(addButton);
fireEvent.change(input, { target: { value: 'Todo 2' } });
fireEvent.click(addButton);
expect(screen.getByTestId('todo-count')).toHaveTextContent('2 items left');
// Complete one todo
const checkbox = screen.getByTestId('todo-checkbox-1');
fireEvent.click(checkbox);
expect(screen.getByTestId('todo-count')).toHaveTextContent('1 items left');
});
});
// Challenge: Add integration tests with a real API
// Challenge: Add performance tests for large todo lists
// Challenge: Add accessibility tests