React-testing-library Versus Enzyme: Which is the winner?

ZipyZipy
11 min read

Author - Aryan Raj

Testing computer programs, especially ones written using React, can be tricky. However, opting for a React testing library can save you a lot of hassle. Enzyme and React-testing-library are two popular options out there.

Enzyme has been in existence for a decent amount of time now, having been released back in 2015. In contrast, react-testing-library was introduced more recently in 2018, and, quickly gained popularity. However, according to the 2019 State of JavaScript Survey, both these libraries are pretty popular tools for testing React.

And in this article, we will look at how React-testing-library and Enzyme stack against each other and even run some tests together. The idea is to help you test out these libraries before you deploy your React app.

What is the Enzyme testing library?

Enzyme is a popular JavaScript testing library primarily used for unit testing in React applications. Often used in conjunction with Jest, it has been available since 2015, resulting in a well-established ecosystem with comprehensive coverage of potential issues. Later in this post, we will provide examples of test cases that utilize Enzyme.

What is React-testing-library?

‎Released in 2018, the React Test‎ing Library gained popularity quickly and is built on top of the DOM Testing Library. This testing library enables developers to write test cases for real events, such as a user clicking on a button, making it equivalent to actual user interaction. Later in this post, we will ‎also demonstrate some test cases using the React Testing Library.

React component testing tools

When it comes to testing React components, developers are increasingly adopting a different mindset. The objective of software testing a feature or a product before it goes to market is to identify and report any issues in the application and be confident about the release.

In terms of testing structure, the difference between Enzyme and react-testing-library is quite apparent. With react-testing-library, it is easy to write tests that accurately represent how the application is experienced by users. When writing tests with react-testing library, you're testing the application as if you are ‎the user who is interacting with the app.

On the other hand, writing tests with Enzyme, while still achieving the same level of confidence as react-testing-library, can be a bit more challenging‎. This is because it can ‎be more difficult to create a test structure that closely resembles how a real user would interact with the application.‎‎

Sample React components suitable for testing

In addition to the main differences between functional and class components in React, several other details may influence your choice of tool for your next project. To illustrate this, I have developed a simple component idea using‎ both approaches, which will also allow us to compare ‎the test structures for each type of component.

The component we will create is called RangeCounter. It will have two control buttons for adding and subtracting, as well as a display of the current count betw‎een these buttons. The count will be determined by the props passed to the component, specifically the minimum and maximum values.‎

When the user reaches either the minimum or maximum value, they will receive an alert message indicating why they are unable to continue incrementing or decrementing the counter.

If you are interested in seeing the complete code, including tests, a‎ GitHub repository is available for you to use alongside this post.

Class component example

/**
 * Class component for RangeCounter.
 * 
 * This component renders two buttons for incrementing and decrementing a counter,
 * as well as a display of the current count between these buttons. The count is
 * determined by the props passed to the component, specifically the minimum and
 * maximum values. When the user reaches either the minimum or maximum value, they
 * will receive an alert message indicating why they are unable to continue
 * incrementing or decrementing the counter.
 */
class RangeCounterClass extends Component {
  constructor(props) {
    super(props);
    const { min } = props;
    this.state = {
      counter: min,
      hasEdited: false
    };
    this.incrementCounter = this.incrementCounter.bind(this);
    this.decrementCounter = this.decrementCounter.bind(this);
  }

  componentDidUpdate() {
    if (this.state.counter === this.props.max || this.state.counter === this.props.min) {
      this.setState({ hasEdited: true });
    }
  }

  incrementCounter() {
    this.setState((prevState) => {
      const { max } = this.props;
      const { counter } = prevState;
      return {
        counter: counter + 1,
        hasEdited: counter + 1 === max || counter === max || counter + 1 === max
      };
    });
  }

  decrementCounter() {
    this.setState((prevState) => {
      const { min } = this.props;
      const { counter } = prevState;
      return {
        counter: counter - 1,
        hasEdited: counter - 1 === min || counter === min || counter - 1 === min
      };
    });
  }

  render() {
    const { max, min } = this.props;
    return (
      <div className="RangeCounter">
        <span className="RangeCounter__title">Class RangeCounter</span>
        <div className="RangeCounter__controls">
          <button
            disabled={this.state.counter <= min}
            onClick={this.decrementCounter}
          >
            -
          </button>
          <span>{this.state.counter}</span>
          <button
            disabled={this.state.counter >= max}
            onClick={this.incrementCounter}
          >
            +
          </button>
        </div>
        {(this.state.counter >= max || this.state.counter <= min) &&
          this.state.hasEdited && (
            <span className="RangeCounter__alert">Range limit reached!</span>
          )}
      </div>
    );
  }
}

Functional component example

/**
 * A functional component that presents a range counter to the user
 * with control buttons for adding and subtracting the current count
 * within the given range limit. If the user reaches the limit, an
 * alert message is displayed below the counter.
 *
 * @param {Object} props - The props object containing the min and max
 * values for the range limit.
 * @returns {JSX.Element} - The rendered RangeCounter component.
 */
const RangeCounterFunctional = props => {
  // Extracting min and max values from the props object
  const { max, min } = props;

  // Defining counter and hasEdited state variables with their initial values
  const [counter, setCounter] = useState(min);
  const [hasEdited, setHasEdited] = useState(false);

  // Defining an effect to set hasEdited state to true if counter has been edited
  useEffect(() => {
    if (counter !== min && !hasEdited) {
      setHasEdited(true);
    }
  }, [counter, hasEdited, min]);

  // Rendering the RangeCounter component
  return (
    <div className="RangeCounter">
      <span className="RangeCounter__title">Functional RangeCounter</span>
      <div className="RangeCounter__controls">
        <button
          disabled={counter <= min}
          onClick={() => setCounter(counter - 1)}
        >
          -
        </button>
        <span data-testid="counter-value">{counter}</span>
        <button
          disabled={counter >= max}
          onClick={() => setCounter(counter + 1)}
        >
          +
        </button>
      </div>
      {(counter >= max || counter <= min) && hasEdited && (
        <span className="RangeCounter__alert">Range limit reached!</span>
      )}
    </div>
  );
};

Testing with Enzyme vs. React-testing-library

For both the class and functional components, we will con‎‎‎‎duct the following tests using each of these testing tools:‎‎

  1. Verify that the user can increase the co‎‎‎‎unt when it is allowed to be incremented.

  2. ‎‎‎‎‎Ensure that the alert message appears only when the user has edited the count and reached either the minimum or maximum limit.

Enzyme Testing: Can the user incre‎‎‎ment the counter when it is allowed?

Let’s have a look at the first scenario using Enz‎‎yme.

/**
 * This test suite uses Enzyme to test the RangeCounterClass component.
 */

describe("RangeCounterClass", () => {
  let wrapper;  
  beforeEach(() => {
    wrapper = shallow(<RangeCounterClass />);
  });

  /**
   * Tests if the counter can be incremented when incrementing is allowed.
   */
  describe("when incrementing counter is allowed", () => {
    it("updates counter value correctly", () => {
      wrapper.instance().incrementCounter();
      expect(wrapper.state().counter).toEqual(1);
      expect(wrapper.state().hasEdited).toEqual(true);
    });
  });
});

This code is a test case for the RangeCo‎‎‎unterClass component using the Enzyme testing library. It tests whether the counter value updates correctly when incrementing is allowed. It sets up a test suite with a describe the block for the RangeCounterClass component and a beforeEach block to create a shallow wrapper of ‎‎the component before each test. Then, there is a describe the block for the scenario where incrementing is allowed, and it blocks that call the incrementCounter method of the component instance and ch‎‎ecks that the state of counter and hasEdited have been upd‎‎‎ated as expected using the expect function.

The test code ensures that the component ‎‎works correctly by checking the received props‎‎ and the component's state‎‎. If the test passes, it is assumed that the displayed count‎‎ is the same as the counter-state variable. The test also verifies whether the ‎‎hasE‎‎dited variable has changed to true after programmatically updating the counter, which indicates whether the ‎‎alert should be displayed or not.‎‎‎

React-testing-library: Can the user incre‎‎ment the counter when it is allowed?

Now let’s try the same test scenario but now let us use the react-testing-library:

describe("RangeCounterFunctional", () => {
  describe("when incrementing counter is allowed", () => {
    it("updates the counter value", async () => {
      const { getByTestId, getByText } = render(<RangeCounterB min={2} />);
      const incrementButton = getByText("+");
      fireEvent.click(incrementButton);
      expect(getByTestId("counter-value").innerHTML).toEqual("3");
    });
  });
});

The above code block is an example of a test written using react-te‎‎‎sting-library. It tests the RangeCounterB functional component to see if‎‎‎ the counter value updates correctly when incrementing is allowed.

First, it renders the component with a minimum value of 2. Then it gets the increment button using getByText and simulate a click on it using fireEvent.click. Finally, it checks if the counter value has updated to 3 using getByTestId and expect.

The purpose of this ‎‎‎test is to verify the user interface display‎‎‎, which is achieved by retrieving the actual DOM element and checking its content. The following three scenarios in the list use a similar approach. The last scenario,‎‎‎ however, is particularly noteworthy as it shows that Enzyme can be used to test the same concept as react-testing-library.

Enzyme Test: Does alert message only show after editing and reaching limit?

describe("RangeCounterClass", () => {
  let wrapper;

  beforeEach(() => {
    // initialize the component to be tested before each test case
    wrapper = shallow(<RangeCounterA />);
  });

  it("shows range reached alert when it reaches limit by clicking control buttons", () => {
    // reinitialize the component with specific props
    wrapper = shallow(<RangeCounterA min={0} max={1} />);

    // simulate an increment action that will reach the maximum limit
    wrapper.instance().incrementCounter();
    wrapper.update();

    // check if the alert message is rendered with the correct text
    const alert = wrapper.find('.RangeCounter__alert');
    expect(alert.text()).toEqual('Range limit reached!');
  });
});

The code block shows a test suite using Enzyme's shallow rendering method to test a component called RangeCounterClass. The beforeEach function is used to initialize the component before each test case.

The it‎‎‎ function describes the behavior being tested, which is if the component displays an alert message when the range limit is reached by clicking the control buttons. The component is re-initialized with specific props to simulate the range limit.

The instance method is used to simulate an‎‎‎‎ increment action that will reach the maximum limit. The update method is then called to trigger a re-render of the component.‎‎‎

The test che‎‎‎‎‎cks if the alert message is rende‎‎‎‎red with the correct text by f‎‎‎‎inding the element with the .RangeCounter__alert class and comparing its text content with the expected value using the toEqual matcher.

React-testing-library Test: Does the alert message only show after editing and reaching the limit?

describe("RangeCounterFunctional", () => {
  it("shows range reached alert when it reaches limit by clicking control buttons",
    () => {
      // render the RangeCounterB component with min and max props
      const { getByText } = render(<RangeCounterB min={0} max={1} />);

      // find the increment button and click it
      const incrementButton = getByText("+");
      fireEvent.click(incrementButton);

      // assert that the alert message is visible
      expect(getByText("Range limit reached!")).toBeVisible();
    }
  );
});

This code is a test case using react-testing-library to check if the "Range limit reached!" alert ‎message has shown up ‎‎‎when the user has reached ‎the max limit by ‎clicking the increment button. It renders the RangeCounterB component with min a‎‎‎d max props and gets the increment button by text. Then, it clicks the button and asserts that the alert message is visible.

How Enzyme and React-testing-libra‎‎‎ry conducted these tests

Both Enzyme and react-testing-libra‎‎‎ry confirm that the alert is displayed on the page, but they do it differently.

Enzyme commonly searches for elements in the‎‎ page by their class, even though users don't see that information in the UI. After having the element, Enzyme checks its contents to ensure that it's what the user sees.

On the other hand, react-testing‎‎‎-library searches directly by the actual text that the user sees. If you have many child components and a more complex HT‎‎‎ML structure, using Enzyme may pose more challenges when following the same concept.

If you're‎‎‎ wondering if you can migrate your tests from one tool to the other, it's possible, but it may require some ‎‎adjustments.

Migrat‎ing from Enzyme t‎o React-testing-library

It is relatively simpler to migrate from E‎‎nzyme to react-testing-library as compared to the reverse. The strategy is to start using both of these libraries in your React app, and then one by one convert your Enzyme tests to RTL tests. Once this is done, you can remove all your Enzyme dependencies and stick to React Testing Library for the future.

Migr‎‎‎ating from React-testing-library to Enzyme

To switch from react-testing-library to Enzyme, you'll need to add an extra library called enzyme-adapter-react-[react-version]. This adapter library is essential, and i‎‎‎ts setup steps vary depending on your React version. Enzyme's adapters currently support up to Re‎‎‎act v.16, and there's an unofficial adapter for React v.17. Unfo‎‎‎rtunately, there's no adapter for React v.18 as of now.

After installing the adapter, you can choose your preferred test runner as Enzyme. Now you can start modifying your tests in RTL to run in Enzyme.

React-testing-library vs Enzyme

Determining whether React-testing-library or Enzyme is better depends on various factors. Here is a brief React-testing-library vs Enzyme comparison.

React-testing-library vs Enzyme

Conclusion

In conclusion, whether you have built your app from scratch or are using Bootstrap in React, both Enzyme and Re‎‎act-testing-library can be good options to explore. However, choosing a React Testing Library ultimately depends on the specific needs of your project.

Re‎‎act-testing-library is better suited for testing user‎‎ behavior, while Enz‎‎‎yme may be better for matching the state of React or other functions with the state. Both tools have their limitations and benefits, and it's important to consider which tool will provide the most effective testing for your particular use case.

Happy testing!

This blog was originally published at Zipy.

0
Subscribe to my newsletter

Read articles from Zipy directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Zipy
Zipy

Zipy is a digital experience platform for capturing session replay, error monitoring, product & website analytics, and error debugging.