
Testing React components with Jest and React Testing Library (RTL) has become the modern standard for creating maintainable, user-centric tests. Here’s a comprehensive guide to effective unit testing:
Core Testing Principles
1. Testing Philosophy
- User-centric tests: Test what users experience, not implementation details
- Accessibility integration: Tests double as basic a11y checks
- Minimal mocking: Mock only external dependencies, not React components
2. Recommended Testing Structure
src/
  components/
    Button/
      Button.tsx
      Button.test.tsx
      Button.module.css
  __mocks__/          # Manual mocks
  test-utils/         # Custom render functionsBasic Test Setup
1. Configuration
// jest.config.js
module.exports = {
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect'],
  moduleNameMapper: {
    '\\.(css|less)$': 'identity-obj-proxy' // CSS module mock
  }
};2. Custom Render Function
// test-utils/customRender.tsx
import { render, RenderOptions } from '@testing-library/react';
import { ReactElement } from 'react';
import { AppProviders } from '../providers/AppProviders';
const customRender = (
  ui: ReactElement,
  options?: Omit<RenderOptions, 'wrapper'>
) => render(ui, { wrapper: AppProviders, ...options });
export * from '@testing-library/react';
export { customRender as render };Component Testing Patterns
1. Basic Component Test
import { render, screen } from '../test-utils/customRender';
import Button from './Button';
test('renders button with correct text', () => {
  render(<Button>Click me</Button>);
  expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument();
});2. Testing Props
test('applies primary variant class', () => {
  render(<Button variant="primary">Submit</Button>);
  expect(screen.getByRole('button')).toHaveClass('primary');
});3. Testing Events
test('calls onClick handler when clicked', () => {
  const handleClick = jest.fn();
  render(<Button onClick={handleClick}>Click</Button>);
  fireEvent.click(screen.getByRole('button'));
  expect(handleClick).toHaveBeenCalledTimes(1);
});4. Async Behavior Testing
test('shows loading state when isLoading prop is true', async () => {
  render(<Button isLoading>Save</Button>);
  expect(screen.getByRole('button')).toBeDisabled();
  expect(screen.getByText(/loading/i)).toBeInTheDocument();
  expect(screen.queryByText(/save/i)).not.toBeInTheDocument();
});Advanced Testing Scenarios
1. Testing Custom Hooks
import { renderHook } from '@testing-library/react-hooks';
import { useCounter } from './useCounter';
test('increments counter', () => {
  const { result } = renderHook(() => useCounter());
  act(() => result.current.increment());
  expect(result.current.count).toBe(1);
});2. Context Providers Testing
test('displays user name from context', () => {
  render(
    <UserContext.Provider value={{ name: 'John' }}>
      <UserProfile />
    </UserContext.Provider>
  );
  expect(screen.getByText(/john/i)).toBeInTheDocument();
});3. Form Testing
test('submits form data', async () => {
  const handleSubmit = jest.fn();
  render(<LoginForm onSubmit={handleSubmit} />);
  userEvent.type(screen.getByLabelText(/email/i), 'test@example.com');
  userEvent.type(screen.getByLabelText(/password/i), 'password123');
  userEvent.click(screen.getByRole('button', { name: /sign in/i }));
  await waitFor(() => 
    expect(handleSubmit).toHaveBeenCalledWith({
      email: 'test@example.com',
      password: 'password123'
    })
  );
});Mocking Strategies
1. API Calls Mocking
// __mocks__/axios.ts
export default {
  get: jest.fn(() => Promise.resolve({ data: [] }))
};
// Component test
import axios from 'axios';
jest.mock('axios');
test('fetches data on mount', async () => {
  (axios.get as jest.Mock).mockResolvedValue({ data: mockData });
  render(<DataFetcher />);
  expect(axios.get).toHaveBeenCalledWith('/api/data');
  await waitFor(() => 
    expect(screen.getByText(mockData[0].name)).toBeInTheDocument()
  );
});2. Component Mocking
// __mocks__/components/ChildComponent.tsx
const MockChild = () => <div>Mock Child</div>;
export default MockChild;
// Parent component test
jest.mock('../ChildComponent');
test('renders parent with mocked child', () => {
  render(<ParentComponent />);
  expect(screen.getByText(/mock child/i)).toBeInTheDocument();
});Best Practices
- Query Priority:
- Prefer getByRolefor most queries
- Use getByTextfor static content
- Fall back to getByTestIdfor dynamic elements
- Assertion Patterns:
   // Good
   expect(element).toBeInTheDocument();
   expect(element).toHaveAttribute('aria-hidden', 'true');
   // Avoid
   expect(element).not.toBeNull();- Test Organization:
   describe('Button component', () => {
     describe('when primary variant', () => {
       test('renders with primary class', () => { /* ... */ });
     });
     describe('when clicked', () => {
       test('calls onClick handler', () => { /* ... */ });
     });
   });Performance Optimization
1. Parallel Testing
// jest.config.js
module.exports = {
  maxWorkers: '80%', // Utilize 80% of available cores
  testEnvironment: 'jsdom'
};2. Snapshot Testing (Use Sparingly)
test('matches snapshot', () => {
  const { asFragment } = render(<Component />);
  expect(asFragment()).toMatchSnapshot();
});3. Test Data Factories
// test-utils/factories.ts
export const createUser = (overrides = {}) => ({
  id: 1,
  name: 'Test User',
  email: 'test@example.com',
  ...overrides
});
// Usage in tests
const adminUser = createUser({ role: 'admin' });Common Pitfalls and Solutions
| Problem | Solution | 
|---|---|
| “Not wrapped in act()” warnings | Use asyncutils likewaitFor | 
| Can’t find element | Verify accessibility attributes | 
| Tests affecting each other | Add afterEach(cleanup) | 
| Slow tests | Mock heavy dependencies | 
Integration with TypeScript
// Extend Jest matchers
declare global {
  namespace jest {
    interface Matchers<R> {
      toBeInTheDocument(): R;
      toHaveAttribute(attr: string, value?: string): R;
    }
  }
}
// Typed queries
const emailInput = screen.getByRole('textbox', { 
  name: /email/i 
}) as HTMLInputElement;