⚡ TECH BLOG
Home
Blog
Tags
About
⚡

Powered by Next.js 15 & Modern Web Tech ⚡

Back to Home

Testing React Applications with Jest and React Testing Library

November 15, 2021
reacttestingjestjavascript
Testing React Applications with Jest and React Testing Library

Testing React Applications with Jest and React Testing Library

Testing is essential for building reliable React applications. This guide covers testing strategies with Jest and React Testing Library.

Setup

Installation

npm install -D jest @testing-library/react @testing-library/jest-dom @testing-library/user-event jest-environment-jsdom

Configuration

// jest.config.js
module.exports = {
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
  moduleNameMapper: {
    '\\.(css|less|scss|sass)$': 'identity-obj-proxy',
    '^@/(.*)$': '<rootDir>/src/$1',
  },
};

// jest.setup.js
import '@testing-library/jest-dom';

Basic Tests

Component Rendering

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>Click me</Button>);
    expect(screen.getByText('Click me')).toBeInTheDocument();
  });

  it('handles click events', async () => {
    const handleClick = jest.fn();
    render(<Button onClick={handleClick}>Click me</Button>);
    
    await userEvent.click(screen.getByText('Click me'));
    
    expect(handleClick).toHaveBeenCalledTimes(1);
  });

  it('is disabled when disabled prop is true', () => {
    render(<Button disabled>Click me</Button>);
    expect(screen.getByText('Click me')).toBeDisabled();
  });
});

Query Methods

Priority Order

// 1. getByRole - most accessible
screen.getByRole('button', { name: 'Submit' });
screen.getByRole('textbox', { name: 'Email' });
screen.getByRole('heading', { name: 'Welcome', level: 1 });

// 2. getByLabelText - for form elements
screen.getByLabelText('Email');
screen.getByLabelText(/password/i);

// 3. getByPlaceholderText
screen.getByPlaceholderText('Enter your email');

// 4. getByText - for non-interactive elements
screen.getByText('Welcome');
screen.getByText(/loading/i);

// 5. getByTestId - last resort
screen.getByTestId('loading-spinner');

Query Variants

// getBy - throws if not found or multiple found
screen.getByText('Hello');

// queryBy - returns null if not found
expect(screen.queryByText('Error')).not.toBeInTheDocument();

// findBy - async, waits for element
await screen.findByText('Loaded');

// All variants
screen.getAllByRole('listitem');
screen.queryAllByText(/item/i);
await screen.findAllByText(/loaded/i);

Testing User Interactions

User Event

import userEvent from '@testing-library/user-event';

describe('Form', () => {
  it('submits form with user input', async () => {
    const user = userEvent.setup();
    const handleSubmit = jest.fn();
    
    render(<ContactForm onSubmit={handleSubmit} />);
    
    await user.type(screen.getByLabelText('Name'), 'John Doe');
    await user.type(screen.getByLabelText('Email'), 'john@example.com');
    await user.click(screen.getByRole('button', { name: 'Submit' }));
    
    expect(handleSubmit).toHaveBeenCalledWith({
      name: 'John Doe',
      email: 'john@example.com',
    });
  });

  it('handles select and checkbox', async () => {
    const user = userEvent.setup();
    
    render(<PreferencesForm />);
    
    await user.selectOptions(
      screen.getByLabelText('Country'),
      'USA'
    );
    
    await user.click(screen.getByLabelText('Subscribe to newsletter'));
    
    expect(screen.getByLabelText('Country')).toHaveValue('USA');
    expect(screen.getByLabelText('Subscribe to newsletter')).toBeChecked();
  });
});

Keyboard Navigation

it('handles keyboard navigation', async () => {
  const user = userEvent.setup();
  
  render(<Dropdown items={['Apple', 'Banana', 'Orange']} />);
  
  await user.click(screen.getByRole('button', { name: 'Open' }));
  await user.keyboard('{ArrowDown}');
  await user.keyboard('{Enter}');
  
  expect(screen.getByText('Banana')).toBeInTheDocument();
});

Testing Async Behavior

Waiting for Updates

import { waitFor, waitForElementToBeRemoved } from '@testing-library/react';

describe('DataFetching', () => {
  it('shows loading then data', async () => {
    render(<UserList />);
    
    expect(screen.getByText('Loading...')).toBeInTheDocument();
    
    await waitForElementToBeRemoved(() => screen.queryByText('Loading...'));
    
    expect(await screen.findByText('John')).toBeInTheDocument();
  });

  it('handles errors', async () => {
    server.use(
      rest.get('/api/users', (req, res, ctx) => 
        res(ctx.status(500))
      )
    );
    
    render(<UserList />);
    
    await waitFor(() => {
      expect(screen.getByText('Error loading users')).toBeInTheDocument();
    });
  });
});

Testing Hooks

import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';

describe('useCounter', () => {
  it('initializes with default value', () => {
    const { result } = renderHook(() => useCounter());
    expect(result.current.count).toBe(0);
  });

  it('increments count', () => {
    const { result } = renderHook(() => useCounter());
    
    act(() => {
      result.current.increment();
    });
    
    expect(result.current.count).toBe(1);
  });
});

Testing Context

const renderWithProvider = (ui, { theme = 'light', ...options } = {}) => {
  return render(
    <ThemeContext.Provider value={{ theme }}>
      {ui}
    </ThemeContext.Provider>,
    options
  );
};

describe('ThemedComponent', () => {
  it('renders with theme', () => {
    renderWithProvider(<Header />, { theme: 'dark' });
    expect(screen.getByRole('banner')).toHaveClass('dark');
  });
});

Mocking

Mocking Modules

// Mock a module
jest.mock('../api/users', () => ({
  fetchUsers: jest.fn(() => Promise.resolve([
    { id: 1, name: 'John' },
    { id: 2, name: 'Jane' },
  ])),
}));

// Mock with implementation
jest.mock('../utils/format', () => ({
  formatDate: (date) => `Formatted: ${date}`,
}));

Mocking Functions

it('calls callback with correct data', () => {
  const mockCallback = jest.fn();
  
  render(<Button onClick={mockCallback} />);
  
  fireEvent.click(screen.getByRole('button'));
  
  expect(mockCallback).toHaveBeenCalledWith(expect.objectContaining({
    timestamp: expect.any(Number),
  }));
});

Snapshot Testing

it('matches snapshot', () => {
  const { asFragment } = render(<Card title="Hello">Content</Card>);
  expect(asFragment()).toMatchSnapshot();
});

// Inline snapshots
it('matches inline snapshot', () => {
  const { container } = render(<Button>Click</Button>);
  expect(container).toMatchInlineSnapshot(`
    <div>
      <button class="btn">Click</button>
    </div>
  `);
});

Integration Testing

describe('Todo App', () => {
  it('adds, completes, and removes todos', async () => {
    const user = userEvent.setup();
    render(<TodoApp />);
    
    // Add todo
    await user.type(screen.getByPlaceholderText('Add todo'), 'Buy milk');
    await user.click(screen.getByRole('button', { name: 'Add' }));
    
    expect(screen.getByText('Buy milk')).toBeInTheDocument();
    
    // Complete todo
    await user.click(screen.getByRole('checkbox', { name: /buy milk/i }));
    expect(screen.getByText('Buy milk')).toHaveClass('completed');
    
    // Remove todo
    await user.click(screen.getByRole('button', { name: /delete buy milk/i }));
    expect(screen.queryByText('Buy milk')).not.toBeInTheDocument();
  });
});

Best Practices

  1. Test behavior, not implementation
  2. Use accessible queries (getByRole, getByLabelText)
  3. Avoid testing library details
  4. Use userEvent over fireEvent
  5. Test async behavior properly
  6. Keep tests isolated
// Bad - testing implementation
expect(component.state('count')).toBe(1);

// Good - testing behavior
expect(screen.getByText('Count: 1')).toBeInTheDocument();

// Bad - using container.querySelector
expect(container.querySelector('.btn-primary')).toBeInTheDocument();

// Good - using accessible query
expect(screen.getByRole('button', { name: 'Submit' })).toBeInTheDocument();

Coverage

// package.json
{
  "scripts": {
    "test:coverage": "jest --coverage"
  },
  "jest": {
    "coverageThreshold": {
      "global": {
        "branches": 80,
        "functions": 80,
        "lines": 80,
        "statements": 80
      }
    }
  }
}

Conclusion

Jest and React Testing Library provide powerful tools for testing React applications. Focus on testing user behavior, use accessible queries, and maintain good test coverage for reliable applications.

Share:

💬 Comments