How to Test Class Components in React

Testing class components in React is essential for maintaining legacy codebases and ensuring application stability. This guide provides a straightforward overview of how to test React class components using Jest alongside React Testing Library and Enzyme. You will learn how to test component rendering, state changes, user interactions, and lifecycle methods to ensure your application behaves as expected.

Choosing Your Testing Tools

When testing React class components, you generally choose between two primary libraries:

  1. React Testing Library (RTL): The modern standard recommended by the React team. It focuses on testing component behavior from the user’s perspective rather than implementation details (like internal state or lifecycle methods).
  2. Enzyme: A legacy testing utility by Airbnb. While deprecated in modern React, it is still widely used in older codebases because it allows direct inspection of a class component’s internal state, props, and lifecycle methods.

React Testing Library encourages testing what the user sees on the screen. It treats class components and functional components exactly the same way.

Example Class Component

Here is a simple Counter class component:

import React, { Component } from 'react';

class Counter extends Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  increment = () => {
    this.setState({ count: this.state.count + 1 });
  };

  render() {
    return (
      <div>
        <p>Current Count: {this.state.count}</p>
        <button onClick={this.increment}>Increment</button>
      </div>
    );
  }
}

export default Counter;

Writing the Test

To test this component using React Testing Library and Jest, you simulate user interaction and assert that the DOM updates correctly.

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Counter from './Counter';

test('increments the count when the button is clicked', async () => {
  render(<Counter />);

  // Verify initial state
  const countText = screen.getByText(/Current Count: 0/i);
  expect(countText).toBeInTheDocument();

  // Simulate user click
  const button = screen.getByRole('button', { name: /increment/i });
  await userEvent.click(button);

  // Verify updated state in the DOM
  expect(screen.getByText(/Current Count: 1/i)).toBeInTheDocument();
});

Testing with Enzyme (Legacy Codebases)

If you are working on an older codebase that utilizes Enzyme, you can write unit tests that directly access the component instance, state, and methods.

Writing the Test with Shallow Rendering

Enzyme’s shallow rendering renders only the single component without rendering its children, making unit tests fast and isolated.

import { shallow } from 'enzyme';
import Counter from './Counter';

describe('Counter Component', () => {
  it('should initialize with a count of 0', () => {
    const wrapper = shallow(<Counter />);
    expect(wrapper.state('count')).toBe(0);
  });

  it('should increment count in state when button is clicked', () => {
    const wrapper = shallow(<Counter />);
    
    // Find button and simulate click
    wrapper.find('button').simulate('click');
    
    // Assert state changed directly
    expect(wrapper.state('count')).toBe(1);
  });

  it('should call the increment method directly', () => {
    const wrapper = shallow(<Counter />);
    const instance = wrapper.instance();

    // Call class method directly
    instance.increment();

    expect(wrapper.state('count')).toBe(1);
  });
});

Testing Lifecycle Methods

If your class component relies on lifecycle methods like componentDidMount or componentWillUnmount, you can test them by asserting their side effects.

Example Component with Lifecycle Method

class DataFetcher extends Component {
  state = { data: null };

  componentDidMount() {
    this.props.fetchData().then((data) => this.setState({ data }));
  }

  render() {
    return <div>{this.state.data ? this.state.data : 'Loading...'}</div>;
  }
}

Testing the Lifecycle with Jest Mocks

You can mock the prop function to test if componentDidMount executes it correctly upon rendering.

import { render, screen, waitFor } from '@testing-library/react';
import DataFetcher from './DataFetcher';

test('fetches data on mount', async () => {
  const mockFetchData = jest.fn().mockResolvedValue('Hello World');

  render(<DataFetcher fetchData={mockFetchData} />);

  // Verify initial loading state
  expect(screen.getByText(/Loading.../i)).toBeInTheDocument();

  // Verify function call on mount
  expect(mockFetchData).toHaveBeenCalledTimes(1);

  // Verify DOM updates after promise resolves
  await waitFor(() => {
    expect(screen.getByText('Hello World')).toBeInTheDocument();
  });
});