Table of Contents
1. Why Jest?
Jest is a delightful JavaScript testing framework with a focus on simplicity. Created by Meta (Facebook), it's the go-to test runner for React projects and works beautifully with any JavaScript codebase.
Key Features
- Zero Configuration — Works out of the box for most JavaScript projects
- Fast Execution — Parallel test running and intelligent test isolation
- Built-in Mocking — Powerful mocking for functions, modules, and timers
- Snapshot Testing — Catch unexpected UI changes automatically
- Code Coverage — Built-in coverage reports with no extra setup
- Rich Assertions — Comprehensive matcher library for any assertion need
| Feature | Jest | Mocha |
|---|---|---|
| Setup | Zero config | Requires config |
| Assertion Library | Built-in (expect) | Needs Chai |
| Mocking | Built-in | Sinon (external) |
| Coverage | Built-in (Istanbul) | Needs nyc |
| Snapshot Testing | Built-in | Not available |
| Parallel Execution | Automatic | Manual setup |
2. Setting Up Jest
For React Projects (with CRA)
# Jest comes pre-installed with Create React App
npx create-react-app my-app
cd my-app
# Just run the tests
npm test
For Vanilla JavaScript Projects
# Install Jest
npm install --save-dev jest
# Add test script to package.json
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
}
jest.config.js
module.exports = {
testEnvironment: 'jsdom', // For React/browser-like environment
collectCoverageFrom: [
'src/**/*.{js,jsx}',
'!src/index.js',
'!src/reportWebVitals.js',
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
setupFilesAfterSetup: ['@testing-library/jest-dom'],
}
3. Test Anatomy
Every Jest test follows a predictable structure using describe, it, and expect.
// math.test.js
describe('Math Operations', () => {
describe('add()', () => {
it('adds two positive numbers', () => {
expect(add(2, 3)).toBe(5)
})
it('adds negative numbers', () => {
expect(add(-1, -3)).toBe(-4)
})
it('handles zero', () => {
expect(add(5, 0)).toBe(5)
})
})
describe('divide()', () => {
it('divides two numbers', () => {
expect(divide(10, 2)).toBe(5)
})
it('throws on division by zero', () => {
expect(() => divide(10, 0)).toThrow('Cannot divide by zero')
})
})
})
it() block should test one specific behavior. This makes failures easier to identify and debug.
4. Matchers & Assertions
Jest's expect() comes with a rich set of matchers for any assertion scenario.
Common Matchers
// Equality
expect(value).toBe(42) // Strict equality (===)
expect(obj).toEqual({ a: 1 }) // Deep equality
// Truthiness
expect(value).toBeTruthy()
expect(value).toBeFalsy()
expect(value).toBeNull()
expect(value).toBeDefined()
// Numbers
expect(price).toBeGreaterThan(10)
expect(price).toBeLessThanOrEqual(100)
expect(price).toBeCloseTo(9.99, 1)
// Strings
expect(name).toContain('Avinash')
expect(name).toMatch(/^A\w+/)
// Arrays
expect(items).toContain('React')
expect(items).toHaveLength(3)
// Objects
expect(user).toHaveProperty('name', 'John')
expect(user).toHaveProperty('email')
expect(user).toMatchObject({
name: 'John',
role: 'developer',
})
// Exceptions
expect(() => badFn()).toThrow()
expect(() => badFn()).toThrow('Error message')
expect(() => badFn()).toThrow(Error)
// Not
expect(value).not.toBe(0)
expect(items).not.toContain('Vue')
DOM Assertions (@testing-library/jest-dom)
import '@testing-library/jest-dom'
// Element visibility
expect(element).toBeVisible()
expect(element).not.toBeInTheDocument()
// Classes & Attributes
expect(button).toHaveClass('active')
expect(link).toHaveAttribute('href', '/about')
// Content
expect(element).toHaveTextContent('Hello')
expect(element).toHaveValue('test@email.com')
// Focus
expect(input).toHaveFocus()
expect(input).not.toHaveFocus()
// Disabled
expect(button).toBeDisabled()
expect(button).not.toBeDisabled()
5. Setup & Teardown
Jest provides hooks to run code before and after each test or test suite.
describe('Database Tests', () => {
let db
// Run once before ALL tests in this describe block
beforeAll(async () => {
db = await createDatabase()
})
// Run before EACH test
beforeEach(async () => {
await db.clearUsers()
await db.insertUser({ name: 'Test User', email: 'test@test.com' })
})
// Run after EACH test
afterEach(async () => {
await db.clearLogs()
})
// Run once after ALL tests
afterAll(async () => {
await db.close()
})
it('fetches user', async () => {
const user = await db.findUser('test@test.com')
expect(user).toBeTruthy()
expect(user.name).toBe('Test User')
})
it('deletes user', async () => {
await db.deleteUser('test@test.com')
const user = await db.findUser('test@test.com')
expect(user).toBeNull()
})
})
6. Mocking & Spying
Mocking isolates your tests from external dependencies and gives you full control over behavior.
Mock Functions
// Create a mock function
const mockFn = jest.fn()
// Call it
mockFn('hello')
mockFn('world')
// Assertions
expect(mockFn).toHaveBeenCalledTimes(2)
expect(mockFn).toHaveBeenCalledWith('hello')
expect(mockFn).toHaveBeenLastCalledWith('world')
// Mock return values
mockFn.mockReturnValue('result')
mockFn.mockReturnValueOnce('first')
mockFn.mockReturnValueOnce('second')
expect(mockFn()).toBe('first')
expect(mockFn()).toBe('second')
expect(mockFn()).toBe('result') // Returns to normal
Mocking Modules
// api.js
export const fetchProjects = async () => {
const res = await fetch('/api/projects')
return res.json()
}
// api.test.js
import { fetchProjects } from './api'
jest.mock('./api')
beforeEach(() => {
jest.clearAllMocks()
})
it('fetches projects', async () => {
const mockData = [{ id: 1, name: 'Project A' }]
fetchProjects.mockResolvedValue(mockData)
const result = await fetchProjects()
expect(result).toEqual(mockData)
expect(fetchProjects).toHaveBeenCalledTimes(1)
})
Mocking API Calls (fetch)
// Mock fetch globally
beforeEach(() => {
global.fetch = jest.fn()
})
afterEach(() => {
jest.restoreAllMocks()
})
it('fetches user data', async () => {
global.fetch.mockResolvedValue({
ok: true,
json: async () => ({ name: 'Avinash', role: 'Developer' }),
})
const user = await getUser(1)
expect(user.name).toBe('Avinash')
expect(fetch).toHaveBeenCalledWith('/api/users/1')
})
it('handles fetch errors', async () => {
global.fetch.mockResolvedValue({
ok: false,
status: 404,
})
await expect(getUser(999)).rejects.toThrow('User not found')
})
Mocking Timers
beforeEach(() => {
jest.useFakeTimers()
})
afterEach(() => {
jest.useRealTimers()
})
it('calls callback after delay', () => {
const callback = jest.fn()
setTimeout(callback, 3000)
expect(callback).not.toHaveBeenCalled()
jest.advanceTimersByTime(3000)
expect(callback).toHaveBeenCalledTimes(1)
})
7. React Testing Library
React Testing Library (RTL) lets you test components the way users interact with them.
Installation
npm install --save-dev @testing-library/react @testing-library/jest-dom @testing-library/user-event
Testing a Button Component
// Button.jsx
export function Button({ children, onClick, disabled }) {
return (
<button
onClick={onClick}
disabled={disabled}
data-testid="btn-submit"
>
{children}
</button>
)
}
// Button.test.jsx
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { Button } from './Button'
describe('Button', () => {
it('renders with text', () => {
render(<Button>Submit</Button>)
expect(screen.getByRole('button', { name: /submit/i })).toBeInTheDocument()
})
it('calls onClick when clicked', async () => {
const user = userEvent.setup()
const handleClick = jest.fn()
render(<Button onClick={handleClick}>Click Me</Button>)
await user.click(screen.getByRole('button', { name: /click me/i }))
expect(handleClick).toHaveBeenCalledTimes(1)
})
it('is disabled when disabled prop is true', () => {
render(<Button disabled>Submit</Button>)
expect(screen.getByRole('button')).toBeDisabled()
})
})
Testing a Form Component
// ContactForm.test.jsx
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { ContactForm } from './ContactForm'
describe('ContactForm', () => {
it('validates required fields', async () => {
const user = userEvent.setup()
render(<ContactForm />)
await user.click(screen.getByRole('button', { name: /send/i }))
expect(screen.getByText('Name is required')).toBeInTheDocument()
expect(screen.getByText('Email is required')).toBeInTheDocument()
})
it('submits form with valid data', async () => {
const user = userEvent.setup()
const handleSubmit = jest.fn()
render(<ContactForm onSubmit={handleSubmit} />)
await user.type(screen.getByLabelText(/name/i), 'John')
await user.type(screen.getByLabelText(/email/i), 'john@test.com')
await user.type(screen.getByLabelText(/message/i), 'Hello!')
await user.click(screen.getByRole('button', { name: /send/i }))
expect(handleSubmit).toHaveBeenCalledWith({
name: 'John',
email: 'john@test.com',
message: 'Hello!',
})
})
})
Finding Elements
// By role (preferred - accessibility-first)
screen.getByRole('button', { name: /submit/i })
screen.getByRole('textbox', { name: /email/i })
screen.getByRole('heading', { name: /welcome/i })
// By label
screen.getByLabelText(/password/i)
// By placeholder
screen.getByPlaceholder(/search/i)
// By text
screen.getByText(/hello world/i)
// By test id
screen.getByTestId('login-form')
// Query variants (return null instead of throwing)
screen.queryByText('Hidden') // Returns null if not found
screen.queryByRole('button') // Returns null if not found
// Async (waits for element to appear)
await screen.findByText('Loaded!') // Waits until found
8. Async Testing
Testing asynchronous code requires special handling for promises, async/await, and API calls.
Async/Await
it('fetches user data', async () => {
const user = await fetchUser(1)
expect(user.name).toBe('Avinash')
expect(user.email).toContain('@')
})
it('handles rejected promises', async () => {
await expect(fetchUser(999))
.rejects
.toThrow('User not found')
})
Testing Custom Hooks
// useCounter.js
import { useState, useCallback } from 'react'
export function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue)
const increment = useCallback(() => setCount(c => c + 1), [])
const decrement = useCallback(() => setCount(c => c - 1), [])
const reset = useCallback(() => setCount(initialValue), [initialValue])
return { count, increment, decrement, reset }
}
// useCounter.test.js
import { renderHook, act } from '@testing-library/react'
import { useCounter } from './useCounter'
describe('useCounter', () => {
it('starts with initial value', () => {
const { result } = renderHook(() => useCounter(10))
expect(result.current.count).toBe(10)
})
it('increments', () => {
const { result } = renderHook(() => useCounter(0))
act(() => result.current.increment())
expect(result.current.count).toBe(1)
})
it('resets to initial value', () => {
const { result } = renderHook(() => useCounter(5))
act(() => result.current.increment())
act(() => result.current.increment())
act(() => result.current.reset())
expect(result.current.count).toBe(5)
})
})
9. Coverage & CI
Running Coverage
# Run with coverage
npm test -- --coverage
# Watch mode with coverage
npm test -- --coverage --watch
Coverage Report Output
----------|---------|----------|---------|---------|
File | % Stmts | % Branch | % Funcs | % Lines |
----------|---------|----------|---------|---------|
All files | 87.5 | 82.3 | 91.2 | 87.5 |
src/ | 92.1 | 88.5 | 95.0 | 92.1 |
App.js | 100.0 | 100.0 | 100.0 | 100.0 |
utils.js| 85.0 | 75.0 | 90.0 | 85.0 |
----------|---------|----------|---------|---------|
GitHub Actions CI
name: Jest Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
cache: 'npm'
- run: npm ci
- run: npm test -- --coverage --watchAll=false
- name: Upload coverage
uses: actions/upload-artifact@v4
with:
name: coverage
path: coverage/
Jest Best Practices Summary
Test Behavior, Not Implementation
// Don't test internal state
expect(component.state.count).toBe(1)
// Test what the user sees
expect(screen.getByText('1 item')).toBeInTheDocument()
Use descriptive test names
// Bad
it('works', () => { ... })
// Good
it('displays an error message when email is invalid', () => { ... })
Keep tests independent
Each test should work in isolation. Don't rely on test execution order or shared mutable state.
Prefer userEvent over fireEvent
// fireEvent fires a synthetic event
fireEvent.click(button)
// userEvent simulates real user interaction
await userEvent.click(button) // Handles focus, keyboard, etc.