Developing Unit Tests for Frontend (Jest)
Jest—standard for JavaScript unit testing. Covers isolated logic: utilities, hooks, reducers, components. Built into Create React App, works with Next.js, Vite.
Setup
npm install -D jest @types/jest jest-environment-jsdom @testing-library/react @testing-library/jest-dom
// jest.config.ts
export default {
testEnvironment: 'jsdom',
setupFilesAfterFramework: ['<rootDir>/jest.setup.ts'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
'\\.(css|scss)$': 'identity-obj-proxy',
},
transform: {
'^.+\\.(ts|tsx)$': ['@swc/jest'],
},
coverageThreshold: {
global: { branches: 70, functions: 80, lines: 80 },
},
};
Testing Utilities
// src/utils/currency.ts
export const formatCurrency = (amount: number, locale = 'en-US', currency = 'USD') =>
new Intl.NumberFormat(locale, { style: 'currency', currency }).format(amount);
// src/utils/currency.test.ts
describe('formatCurrency', () => {
it('formats USD correctly', () => {
expect(formatCurrency(99.99)).toBe('$99.99');
});
it('handles zero', () => {
expect(formatCurrency(0)).toBe('$0.00');
});
it('formats EUR', () => {
expect(formatCurrency(99.99, 'de-DE', 'EUR')).toBe('99,99 €');
});
});
Testing React Components
// components/Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Button } from './Button';
describe('Button', () => {
it('renders label', () => {
render(<Button>Save</Button>);
expect(screen.getByRole('button', { name: 'Save' })).toBeInTheDocument();
});
it('calls onClick', async () => {
const onClick = jest.fn();
render(<Button onClick={onClick}>Click me</Button>);
await userEvent.click(screen.getByRole('button'));
expect(onClick).toHaveBeenCalledTimes(1);
});
it('disabled button does not fire onClick', async () => {
const onClick = jest.fn();
render(<Button onClick={onClick} disabled>Disabled</Button>);
await userEvent.click(screen.getByRole('button'));
expect(onClick).not.toHaveBeenCalled();
});
it('shows loading spinner when loading', () => {
render(<Button loading>Save</Button>);
expect(screen.getByRole('button')).toHaveAttribute('aria-busy', 'true');
expect(screen.getByTestId('spinner')).toBeInTheDocument();
});
});
Testing Custom Hooks
// hooks/useCounter.test.ts
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';
describe('useCounter', () => {
it('initializes with given value', () => {
const { result } = renderHook(() => useCounter(5));
expect(result.current.count).toBe(5);
});
it('increments', () => {
const { result } = renderHook(() => useCounter(0));
act(() => result.current.increment());
expect(result.current.count).toBe(1);
});
it('respects max value', () => {
const { result } = renderHook(() => useCounter(10, { max: 10 }));
act(() => result.current.increment());
expect(result.current.count).toBe(10);
});
});
Mocking API Requests
// services/api.test.ts
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { fetchUser } from './api';
const server = setupServer(
rest.get('/api/users/:id', (req, res, ctx) => {
return res(ctx.json({ id: 1, name: 'John' }));
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
test('fetchUser returns user data', async () => {
const user = await fetchUser(1);
expect(user.name).toBe('John');
});
test('fetchUser handles 404', async () => {
server.use(
rest.get('/api/users/:id', (req, res, ctx) => res(ctx.status(404)))
);
await expect(fetchUser(999)).rejects.toThrow('Not found');
});
Coverage and CI
# .github/workflows/test.yml
- name: Run tests
run: npm test -- --coverage --ci --maxWorkers=2
- name: Upload coverage
uses: codecov/codecov-action@v3
Timeline
Setting up Jest + first 30–50 tests for existing project: 3–5 days.







