Testing
ship with confidence. break it before users do.
why test
testing is not about proving code works. it is about finding out when it breaks — before your users do.
without tests: - you change one thing, three other things break silently - you are afraid to refactor because something might break - you manually click through the app after every change - bugs reach production
with tests: - change anything, run tests, immediately see what broke - refactor freely — tests catch regressions - document how code is supposed to work - ship features faster (less manual checking)
the testing pyramid:
/\
/E2E\ few — slow, test whole app
/──────\
/Integr. \ some — test components together
/──────────\
/ Unit \ many — fast, test single functions
/______________\
types of tests
unit tests — test a single function or component in isolation. fast, lots of them.
integration tests — test multiple units working together. component + API call, for example.
end-to-end (E2E) tests — test the whole app like a real user. open browser, click buttons, check result. slow but most realistic.
testing JavaScript with Jest
Jest is the most popular JavaScript testing framework.
# install
npm install -D jest @types/jest
# for React
npm install -D jest @testing-library/react @testing-library/jest-dom @testing-library/user-eventunit testing functions
// utils.js — the function we want to test
export function add(a, b) {
return a + b
}
export function isValidEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
}
export function formatDate(date) {
return new Date(date).toLocaleDateString('en-IN')
}
export function truncate(str, maxLength) {
if (str.length <= maxLength) return str
return str.slice(0, maxLength) + '...'
}// utils.test.js — test file
import { add, isValidEmail, formatDate, truncate } from './utils'
// describe — group related tests
describe('add', () => {
// test — single test case
test('adds two positive numbers', () => {
expect(add(1, 2)).toBe(3)
})
test('adds negative numbers', () => {
expect(add(-1, -2)).toBe(-3)
})
test('adds zero', () => {
expect(add(5, 0)).toBe(5)
})
})
describe('isValidEmail', () => {
test('returns true for valid email', () => {
expect(isValidEmail('abhishek@example.com')).toBe(true)
})
test('returns false for missing @', () => {
expect(isValidEmail('abhishekexample.com')).toBe(false)
})
test('returns false for empty string', () => {
expect(isValidEmail('')).toBe(false)
})
})
describe('truncate', () => {
test('returns string unchanged if under limit', () => {
expect(truncate('hello', 10)).toBe('hello')
})
test('truncates and adds ellipsis if over limit', () => {
expect(truncate('hello world', 5)).toBe('hello...')
})
test('returns exact length string unchanged', () => {
expect(truncate('hello', 5)).toBe('hello')
})
})Jest matchers
// equality
expect(value).toBe(3) // strict equality (===)
expect(value).toEqual({a: 1}) // deep equality (objects/arrays)
expect(value).not.toBe(3) // negation
// truthiness
expect(value).toBeTruthy()
expect(value).toBeFalsy()
expect(value).toBeNull()
expect(value).toBeUndefined()
expect(value).toBeDefined()
// numbers
expect(value).toBeGreaterThan(3)
expect(value).toBeLessThan(10)
expect(value).toBeCloseTo(0.3) // floating point comparison
// strings
expect(str).toMatch(/pattern/)
expect(str).toContain('substring')
expect(str).toHaveLength(5)
// arrays
expect(arr).toContain(3)
expect(arr).toHaveLength(3)
expect(arr).toEqual([1, 2, 3])
// objects
expect(obj).toHaveProperty('name')
expect(obj).toHaveProperty('name', 'Abhishek')
expect(obj).toMatchObject({ name: 'Abhishek' }) // partial match
// functions/errors
expect(() => riskyFn()).toThrow()
expect(() => riskyFn()).toThrow('error message')
expect(() => riskyFn()).toThrow(TypeError)
// async
await expect(promise).resolves.toBe('value')
await expect(promise).rejects.toThrow('error')testing async code
// function that fetches data
async function fetchUser(id) {
const res = await fetch(`/api/users/${id}`)
if (!res.ok) throw new Error('User not found')
return res.json()
}
// test with mock fetch
global.fetch = jest.fn()
describe('fetchUser', () => {
beforeEach(() => {
fetch.mockClear() // reset mock between tests
})
test('returns user data on success', async () => {
const mockUser = { id: 1, name: 'Abhishek' }
fetch.mockResolvedValueOnce({
ok: true,
json: async () => mockUser
})
const user = await fetchUser(1)
expect(user).toEqual(mockUser)
expect(fetch).toHaveBeenCalledWith('/api/users/1')
})
test('throws error when user not found', async () => {
fetch.mockResolvedValueOnce({ ok: false })
await expect(fetchUser(999)).rejects.toThrow('User not found')
})
})testing React components
// Button.jsx
function Button({ label, onClick, disabled = false }) {
return (
<button onClick={onClick} disabled={disabled}>
{label}
</button>
)
}
// GoalItem.jsx
function GoalItem({ goal, onToggle, onDelete }) {
return (
<div data-testid="goal-item">
<input
type="checkbox"
checked={goal.completed}
onChange={() => onToggle(goal.id)}
aria-label={goal.title}
/>
<span>{goal.title}</span>
<button onClick={() => onDelete(goal.id)}>Delete</button>
</div>
)
}// Button.test.jsx
import { render, screen, fireEvent } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import Button from './Button'
describe('Button', () => {
test('renders with label', () => {
render(<Button label="Click me" onClick={() => {}} />)
expect(screen.getByText('Click me')).toBeInTheDocument()
})
test('calls onClick when clicked', async () => {
const user = userEvent.setup()
const handleClick = jest.fn()
render(<Button label="Click me" onClick={handleClick} />)
await user.click(screen.getByText('Click me'))
expect(handleClick).toHaveBeenCalledTimes(1)
})
test('does not call onClick when disabled', async () => {
const user = userEvent.setup()
const handleClick = jest.fn()
render(<Button label="Click me" onClick={handleClick} disabled />)
await user.click(screen.getByText('Click me'))
expect(handleClick).not.toHaveBeenCalled()
})
})
// GoalItem.test.jsx
describe('GoalItem', () => {
const mockGoal = { id: '1', title: 'Study React', completed: false }
test('renders goal title', () => {
render(<GoalItem goal={mockGoal} onToggle={() => {}} onDelete={() => {}} />)
expect(screen.getByText('Study React')).toBeInTheDocument()
})
test('shows unchecked checkbox for incomplete goal', () => {
render(<GoalItem goal={mockGoal} onToggle={() => {}} onDelete={() => {}} />)
expect(screen.getByRole('checkbox')).not.toBeChecked()
})
test('calls onToggle when checkbox clicked', async () => {
const user = userEvent.setup()
const onToggle = jest.fn()
render(<GoalItem goal={mockGoal} onToggle={onToggle} onDelete={() => {}} />)
await user.click(screen.getByRole('checkbox'))
expect(onToggle).toHaveBeenCalledWith('1')
})
test('calls onDelete when delete button clicked', async () => {
const user = userEvent.setup()
const onDelete = jest.fn()
render(<GoalItem goal={mockGoal} onToggle={() => {}} onDelete={onDelete} />)
await user.click(screen.getByText('Delete'))
expect(onDelete).toHaveBeenCalledWith('1')
})
})testing Library queries
// find elements
screen.getByText('Hello') // by text content — throws if not found
screen.getByRole('button') // by ARIA role
screen.getByRole('button', { name: 'Submit' })
screen.getByLabelText('Email') // by label
screen.getByPlaceholderText('Search')
screen.getByTestId('goal-item') // by data-testid
screen.getByAltText('Profile photo')
// find multiple
screen.getAllByText('Item')
// won't throw if not found
screen.queryByText('Hello') // null if not found
screen.queryAllByText('Item') // [] if not found
// async — wait for element to appear
await screen.findByText('Loaded!') // waits up to 1 second
await screen.findByRole('button')
// queries by priority (use in this order):
// getByRole → getByLabelText → getByText → getByTestIdmocking
mocking replaces real dependencies with fake ones so tests run fast and predictably.
// mock a module
jest.mock('../lib/api', () => ({
goalsApi: {
list: jest.fn(),
create: jest.fn(),
update: jest.fn(),
delete: jest.fn()
}
}))
import { goalsApi } from '../lib/api'
test('loads and displays goals', async () => {
goalsApi.list.mockResolvedValueOnce({
data: [
{ id: '1', title: 'Study React', completed: false },
{ id: '2', title: 'Read docs', completed: true }
]
})
render(<GoalsList />)
// wait for data to load
await screen.findByText('Study React')
expect(screen.getByText('Study React')).toBeInTheDocument()
expect(screen.getByText('Read docs')).toBeInTheDocument()
})
// mock individual function
const mockFn = jest.fn()
mockFn.mockReturnValue(42)
mockFn.mockResolvedValue({ data: [] }) // async
mockFn.mockRejectedValue(new Error('fail')) // async error
mockFn.mockImplementation((x) => x * 2)
// spy on existing function
const spy = jest.spyOn(console, 'error')
spy.mockImplementation(() => {}) // silence console.error in teststesting Python with pytest
pip install pytest pytest-asyncio httpx# utils.py
def add(a, b):
return a + b
def is_valid_email(email):
import re
return bool(re.match(r'^[^\s@]+@[^\s@]+\.[^\s@]+$', email))
def truncate(text, max_length):
if len(text) <= max_length:
return text
return text[:max_length] + '...'# test_utils.py
import pytest
from utils import add, is_valid_email, truncate
# basic test
def test_add():
assert add(1, 2) == 3
assert add(-1, -2) == -3
assert add(5, 0) == 5
# parametrize — run same test with different inputs
@pytest.mark.parametrize("email, expected", [
("abhishek@example.com", True),
("abhishekexample.com", False),
("", False),
("a@b.c", True),
])
def test_is_valid_email(email, expected):
assert is_valid_email(email) == expected
# test exceptions
def test_divide_by_zero():
with pytest.raises(ZeroDivisionError):
result = 10 / 0
# fixtures — shared setup
@pytest.fixture
def sample_user():
return { "id": 1, "name": "Abhishek", "email": "a@b.com" }
def test_user_name(sample_user):
assert sample_user["name"] == "Abhishek"testing FastAPI endpoints
# test_api.py
from fastapi.testclient import TestClient
from main import app
client = TestClient(app)
def test_get_goals_unauthorized():
response = client.get("/api/goals")
assert response.status_code == 401
def test_create_goal():
# login first
login_res = client.post("/api/auth/login", json={
"email": "test@test.com",
"password": "password123"
})
token = login_res.json()["token"]
# create goal
response = client.post("/api/goals",
json={"title": "Study Testing", "date": "2026-03-06"},
headers={"Authorization": f"Bearer {token}"}
)
assert response.status_code == 201
data = response.json()["data"]
assert data["title"] == "Study Testing"
assert data["completed"] == False
def test_create_goal_empty_title():
response = client.post("/api/goals",
json={"title": "", "date": "2026-03-06"},
headers={"Authorization": f"Bearer test_token"}
)
assert response.status_code == 422 # validation errortest setup and teardown
// Jest lifecycle hooks
beforeAll(() => {
// runs once before all tests in file
// setup database connection
})
afterAll(() => {
// runs once after all tests
// close database connection
})
beforeEach(() => {
// runs before each test
// reset mocks
jest.clearAllMocks()
})
afterEach(() => {
// runs after each test
// cleanup DOM
})# pytest fixtures with scope
@pytest.fixture(scope="session")
def db():
# runs once for whole test session
connection = create_test_db()
yield connection
connection.close()
@pytest.fixture(scope="function") # default
def user(db):
# runs for each test function
user = db.create_user(name="Test", email="test@test.com")
yield user
db.delete_user(user.id) # cleanup after testwhat to test and what not to
DO test:
pure functions with complex logic
components: renders correctly, handles user interaction
API endpoints: correct status codes, response format
error cases: what happens when things go wrong
edge cases: empty arrays, null values, long strings
DON'T test:
third-party libraries (they have their own tests)
implementation details (test behavior, not how)
trivial code (getters/setters, simple assignments)
everything — 80% coverage is fine, 100% is wasteful
running tests
# run all tests
npx jest
# watch mode (re-run on file change)
npx jest --watch
# run specific file
npx jest Button.test.jsx
# run tests matching pattern
npx jest --testNamePattern="login"
# coverage report
npx jest --coverage
# Python
pytest
pytest -v # verbose
pytest test_utils.py # specific file
pytest -k "email" # tests matching pattern
pytest --cov=. # coverage=^._.^= untested code is broken code you haven't found yet