React testing mistakes to avoid (and how to fix them)

React intermediate 9 min read

Who This Is For:

react-developers qa-engineers frontend-testers

React testing mistakes to avoid (and how to fix them)

The Problem

React testing often goes wrong due to implementation-focused tests, fragile selectors, and improper mocking strategies. Teams commonly write tests that break with minor UI changes, test implementation details instead of user behavior, and create false confidence through poorly designed test suites. These mistakes lead to high maintenance costs, missed bugs, and developers losing trust in their test suite.

Why This Matters

Poor tests are worse than no tests. They create a false sense of security, slow down development with constant failures, and become a maintenance nightmare. When tests focus on implementation details rather than user behavior, they resist refactoring and actually hinder code improvements. Good tests, however, catch real bugs, enable confident refactoring, and serve as living documentation of your application’s behavior.

The Solution: User-Centric Testing

The solution is to test your React components from the user’s perspective using React Testing Library’s philosophy: “The more your tests resemble the way your software is used, the more confidence they can give you.” Focus on what users see and interact with, not how components are implemented internally. This approach creates resilient tests that survive refactoring and catch actual user-facing bugs.

How to Implement

Phase 1: Adopt the Right Mindset

Test Behavior, Not Implementation:

// Bad: Testing implementation details
test('renders with correct class names', () => {
  render(<Button primary />);
  expect(screen.getByRole('button')).toHaveClass('btn-primary');
  expect(screen.getByRole('button')).toHaveClass('btn-large');
});

// Good: Testing user behavior
test('calls onClick when clicked', () => {
  const handleClick = jest.fn();
  render(<Button onClick={handleClick}>Click me</Button>);

  fireEvent.click(screen.getByRole('button', { name: /click me/i }));
  expect(handleClick).toHaveBeenCalledTimes(1);
});

Use Accessible Queries:

// Bad: Using test IDs as first choice
render(<UserProfile />);
expect(screen.getByTestId('user-name')).toBeInTheDocument();

// Good: Using accessible queries
render(<UserProfile />);
expect(screen.getByRole('heading', { name: /john doe/i })).toBeInTheDocument();

Phase 2: Fix Common Testing Mistakes

Mistake 1: Testing Private State

// Bad: Testing internal state
test('increments counter', () => {
  const { result } = renderHook(() => useCounter());
  act(() => {
    result.current.increment();
  });
  expect(result.current.count).toBe(1);
});

// Good: Testing component behavior
test('displays updated count when button is clicked', () => {
  render(<Counter />);

  expect(screen.getByText('Count: 0')).toBeInTheDocument();

  fireEvent.click(screen.getByRole('button', { name: /increment/i }));
  expect(screen.getByText('Count: 1')).toBeInTheDocument();
});

Mistake 2: Over-mocking

// Bad: Mocking everything
jest.mock('axios');
test('loads user data', async () => {
  axios.get.mockResolvedValue({ data: { name: 'John' } });

  render(<UserProfile userId="123" />);

  expect(await screen.findByText('John')).toBeInTheDocument();
});

// Good: Mock only external dependencies
jest.mock('../api/userService');
test('loads user data', async () => {
  userService.getUser.mockResolvedValue({ name: 'John' });

  render(<UserProfile userId="123" />);

  expect(await screen.findByText('John')).toBeInTheDocument();
});

Mistake 3: Testing Multiple Behaviors in One Test

// Bad: Testing too many things
test('form validation and submission', () => {
  render(<ContactForm />);

  // Test empty submission
  fireEvent.click(screen.getByRole('button', { name: /submit/i }));
  expect(screen.getByText(/name is required/i)).toBeInTheDocument();

  // Test valid submission
  fireEvent.change(screen.getByLabelText(/name/i), {
    target: { value: 'John' },
  });
  fireEvent.click(screen.getByRole('button', { name: /submit/i }));
  expect(screen.getByText(/form submitted/i)).toBeInTheDocument();
});

// Good: Separate concerns
test('shows validation errors for empty fields', () => {
  render(<ContactForm />);

  fireEvent.click(screen.getByRole('button', { name: /submit/i }));
  expect(screen.getByText(/name is required/i)).toBeInTheDocument();
});

test('submits form with valid data', () => {
  render(<ContactForm />);

  fireEvent.change(screen.getByLabelText(/name/i), {
    target: { value: 'John' },
  });
  fireEvent.click(screen.getByRole('button', { name: /submit/i }));
  expect(screen.getByText(/form submitted/i)).toBeInTheDocument();
});

Phase 3: Implement Testing Best Practices

Use Proper Async Testing:

// Bad: Using arbitrary timeouts
test('loads data', async () => {
  render(<DataLoader />);
  await new Promise((resolve) => setTimeout(resolve, 1000));
  expect(screen.getByText('Data loaded')).toBeInTheDocument();
});

// Good: Using waitFor and findBy queries
test('loads data', async () => {
  render(<DataLoader />);
  expect(await screen.findByText('Data loaded')).toBeInTheDocument();
});

// Alternative: Using waitFor for complex conditions
test('shows loading state then data', async () => {
  render(<DataLoader />);

  expect(screen.getByText('Loading...')).toBeInTheDocument();

  await waitFor(() => {
    expect(screen.getByText('Data loaded')).toBeInTheDocument();
  });

  expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
});

Test Component Integration:

// Good: Testing component integration
test('complete user flow', async () => {
  render(<App />);

  // Navigate to login
  fireEvent.click(screen.getByRole('link', { name: /login/i }));

  // Fill login form
  fireEvent.change(screen.getByLabelText(/email/i), {
    target: { value: '[email protected]' },
  });
  fireEvent.change(screen.getByLabelText(/password/i), {
    target: { value: 'password123' },
  });

  // Submit form
  fireEvent.click(screen.getByRole('button', { name: /login/i }));

  // Verify successful login
  await waitFor(() => {
    expect(screen.getByRole('heading', { name: /welcome/i })).toBeInTheDocument();
  });
});

Results You Can Expect

  • 80% reduction in test maintenance with user-centric testing
  • 60% fewer false positives from implementation-focused tests
  • Improved developer confidence in refactoring and changes
  • Better bug detection through behavior-focused test scenarios

Common Questions

Q: When should I use test IDs? Use test IDs as a last resort when elements have no accessible text or role. They’re useful for dynamic content, icons without labels, or when testing specific component instances.

Q: How much should I mock? Mock only external dependencies like API calls, browser APIs, or third-party libraries. Avoid mocking React components or your own code unless absolutely necessary.

Q: Should I test CSS styles? Generally no. Test that styles achieve their purpose (like hiding/showing elements) rather than specific CSS classes or properties. Use visual testing tools for appearance testing.

Tools & Resources

  • React Testing Library - Essential for user-centric testing
  • Jest - JavaScript testing framework
  • MSW (Mock Service Worker) - API mocking without touching implementation
  • Testing Playground - Helps find the best queries for your tests

Testing Fundamentals

React Components & Architecture

Debugging & Performance

Error Handling & Forms

Need Help With Implementation?

While these testing practices can significantly improve your test quality, implementing them effectively requires understanding testing philosophy, component architecture, and how to balance test coverage with maintainability. Built By Dakic specializes in helping teams establish robust testing strategies that catch real bugs while enabling confident refactoring. Get in touch for a free consultation and discover how we can help you build reliable, maintainable test suites.

Related Topics

Need Help With Implementation?

While these steps provide a solid foundation, proper implementation often requires expertise and experience.

Get Free Consultation