Will Soares Software Developer at Codeminer42.

Testing React components: react-testing-library vs. Enzyme

7 min read 2157

react-testing-library vs. Enzyme

​​Editor’s note: This article was updated on 7 April 2022 to reflect the most recent information for Enzyme and react-testing-library, as well as how to migrate between the two tools.

In a world of myriad tools, it can be difficult to choose the one best suited for your purposes in different scenarios.

The number of variables we should take into consideration is just way too high, and we constantly find ourselves struggling to pick a tool and finally start doing some real work.

This also holds true in the software testing world, especially when you talk about testing your JavaScript code.

One of the things that might decrease the number of options you have available is the technology you’re already using and are not willing to abandon.

When we talk about testing React components, for example, we might end up with a handful of options that you should consider according to how well they are used in the community and how easily you can get information on them through the documentation.

We can talk about tools like Enzyme, react-testing-library, or React Test Renderer, which have rich documentation and use cases available in the React community.

For this post, we’ll be reviewing two of those tools: Enzyme and react-testing-library. We’ll also discuss how to migrate from Enzyme to react-testing-library and how to migrate from react-testing-library to Enzyme.

We’ll cover the following:

While Enzyme has been around for a long time now (released back in 2015), react-testing-library was released more recently (2018) and gained a lot of traction quickly, as noted in the 2019 State of JavaScript Survey.

We made a custom demo for .
No really. Click here to check it out.

React-testing and other tool usage compared in the State of JavaScript survey.
React-testing and other tool usage compared in the State of JavaScript survey.

Context for React components testing tools

It seems like more and more developers are willing to move to a different mindset when it comes to testing React components: after all, the goal of testing software is to be confident of what we’re shipping and to have a better way of debugging things when they go wrong.

For Enzyme and react-testing-library, the difference in the test structure is pretty clear.

With react-testing-library, you’re able to easily write tests that represent well enough how the application is experienced by users.

Let’s say that when you write your tests with react-testing-library, you’re testing your application as if you were the user interacting with the application’s interface.

On the other hand, when you’re writing your tests with Enzyme, even though you are also able to achieve the same level of confidence that you might get with react-testing-library, it is a bit more cumbersome to build your test structure in a way that resembles a real user.

In general, what you might see in codebases when looking at tests with Enzyme is that you’re actually testing the props and state of your components, meaning you are testing the internal behavior of components to confirm that the correct view is being presented to users.

It works something like this: if all these props and state variables have this value, then we assume that the interface presented to the user is what we expect it to be.

Example React components for testing

Besides the two main differences mentioned, you have several details that might help you choose one tool for your next React project (or maybe use both! Why not?)

To demonstrate that, I’ve come up with a simple component idea implemented through two different approaches: one being a functional component with React Hooks, and the other being a class component.

The reason being that we’ll also be able to compare the test structure for each type of component.
If you want to take a look at the entire code (with tests), here’s a GitHub repo you can use alongside this post.

Also, keep in mind that this post does not focus on the setup of any of those tools.

If you want to check how that was done, you can look at this other LogRocket post showing what dependencies are needed for each tool. Additionally, you can check out the GitHub repos for Enzyme and react-testing-library.

So, we’re creating a RangeCounter component that should present two control buttons to users (for adding and subtracting) and the current count in between those buttons.

That count should be ruled by the props passed to the component (min and max).

When the user reaches any of the values in the range limit, they should see an alert message below the counter explaining why they are not able to keep incrementing or decrementing the counter.

Example class component

The class component looks something like this:

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() { ... }
  incrementCounter() { ... }
  decrementCounter() { ... }

  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>
    );
  }
}

Keep in mind that you can always check the GitHub repo for the entire component code.

Example functional component

The functional component will look like this:

const RangeCounterFunctional = props => {
  const { max, min } = props;
  const [counter, setCounter] = useState(min);
  const [hasEdited, setHasEdited] = useState(false);

  useEffect(() => {
    if (counter !== min && !hasEdited) {
      setHasEdited(true);
    }
  }, [counter, hasEdited, min]);

  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>
  );
};

Both have the same behavior and will look mostly the same for users (except for the title, which can be ignored for this post’s purposes).

Testing with Enzyme vs. react-testing-library

We’ll be testing the following scenarios for both components with both tools:

  • Testing that a user is able to increment when incrementing is allowed
  • Testing that a user is able to decrement when decrementing is allowed
  • Testing that a user is not able to increment when count reaches maximum
  • Testing that a user is not able to decrement when count reaches minimum
  • Testing that alert message shows up only after editing and reaching minimum or maximum limit

Testing with Enzyme: Is user able to increment when incrementing is allowed?

Let’s look at the test for the first scenario in the list when using Enzyme:

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

  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);
    });
  });
});

You’ll notice that in order to test that the component works correctly, you have to check that the correct props were received and also that the state looks correct. When that test passes, we assume that the current count showing up to the user is the one that is in the counter state variable.

Also, we check if the hasEdited variable changed to true now that we programmatically updated the counter (the value in that state can also tell us whether the alert will show up or not).

Testing with react-testing-library: Is user able to increment when incrementing is allowed?

Now let’s look at that same test scenario but with 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");
    });
  });
});

It’s clear that the idea of this test is to check what is showing up in the UI. That is accomplished by getting the actual DOM element and checking its content, which represents what the user actually sees.

The next three scenarios in the list display the same kind of pattern. The interesting one to look at now is the last scenario, in which you can see that you can also use Enzyme following the same concept of react-testing-library.

Let’s take a look.

Testing with Enzyme: Does message only show after editing and reaching limit?

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

  it("shows range reached alert when reached limit by clicking control buttons",
    () => {
      wrapper = shallow(<RangeCounterA min={0} max={1}  />);
      wrapper.instance().incrementCounter();
      wrapper.update();
      const alert = wrapper.find('.RangeCounter__alert');
      expect(alert.text()).toEqual('Range limit reached!');
    }
  );
});

Testing with react-testing-library: Does message only show after editing and reaching limit?

describe("RangeCounterFunctional", () => {
  it("shows range reached alert when reached limit by clicking control buttons",
    () => {
      const { getByText } = render(<RangeCounterB min={0} max={1} />);
      const incrementButton = getByText("+");
      fireEvent.click(incrementButton);
      expect(getByText("Range limit reached!")).toBeVisible();
    }
  );
});

We see that both are strictly confirming that the alert is showing up in the page, but in a slightly different way.

With Enzyme, it’s common to see tests that try to find elements in the page by their class (that is not a rule though), which is not meaningful because users do not see those in the UI. After having the element, you can check the contents of it (which is what the user actually sees).

With react-testing-library, the idea is that you search directly by the actual text that the user sees without the overhead work of finding the element that contains that text.

Imagine a scenario where you have tons of child components and a more tangled HTML structure. You’d probably have more trouble following the same concept when using Enzyme.

After reading this post, you may be wondering if you can migrate your tests from one of these tools to the other. Let’s take a look.

Migrating from react-testing-library to Enzyme

To migrate tests from react-testing-library to Enzyme, you’ll need to install an additional library called enzyme-adapter-react-[react-version]. This adapter library is necessary and there are different setup steps depending on your version. Here is a list with all the versions. However, at the time of writing, Enzyme’s adapters only go up to React v.16. An unofficial adapter exists for React v.17, but none yet for React v.18.

If that is not an issue for you, then install the adapter library and choose your test runner. Enzyme isn’t opinionated and offers many different options (e.g., Jest, Mocha, and others). Here’s a list of all the different guides.

Migrating from Enzyme to react-testing-library

Migrating tests from Enzyme to react-testing-library is a little more straightforward. In fact, Kent C. Dodds, the creator of React Testing Library, wrote a complete guide to help developers migrate their tests easily.The guide includes all the necessary installation steps, as well as multiple examples for adapting your tests.

Conclusion

No tool is objectively better than the other: you must consider the variables you have to account for when making a decision about which tool to use.

This specific comparison is based on how developers used to think about tests when using these tools, and how easy it is to follow the idea of testing user behavior instead of component implementation with each tool.

It’s clear that react-testing-library makes that a lot easier with all of the helper methods for querying and the matchers from jest-dom, so it’s natural that you’d want to use that instead.

However, there are limitations to react-testing-library, such as not being able to access component state (which might be intentional because you shouldn’t be doing that in theory).

However, if you feel like you really need that, then Enzyme may be a better option. Just make sure that you write tests that resemble user experience whenever possible.

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Will Soares Software Developer at Codeminer42.

3 Replies to “Testing React components: react-testing-library vs. Enzyme”

  1. That is just an amazing and great comparison. Very elaborate, yet concise.
    Thank you very much.
    It’s like bkack-box testing (react-testing-library) versus white-box testing (enzyme) or BDD (react-testing-library) versus unit-testing (Enzyme).
    This blog certainly made me continue in the direction of react-testing-library.

  2. Hey Jarl, thanks for the feedback!

    That’s exactly how I think about those two tools and the reason why I think people should look more into tools that test user behavior over code. In general it gives you more confidence on how users are in fact perceiving your app.

  3. Thanks for writing this up, though as a fan of Enzyme, I feel like it’s being a bit misrepresented here.

    1) In enzyme you absolutely can simulate a user click:
    `wrapper.find(SELECTOR).simulate(‘click’)`
    And from there the developer can choose how they want to assert that it was handled correctly (state value, or actual display)

    2) While it is true that RTL allows for more user-facing ways of interacting with the code, it seems to do so at the expense of allowing many other developer-only ways of interacting with the code (without polluting production).

    If I want to test that a certain sub-component ( or ) is rendered given certain business logic conditions, with RTL I have two options:
    A) Peek into the downstream HTML and confirm it’s there
    B) Apply some sort of additional label, like a data-testid

    A is faulty since it balloons the scope of tests, and B feels like a code smell of including test-only code in production files.

    Ultimately, it’s possible that I just need to give up the idea that certain things are ever testable in the clear-cut way that I’ve grown accustomed to, and embrace this more ‘hit and run’ style of testing. I just can’t shake the feeling that I’m compromising too much on the core ideology of my test code, which is to help prevent accidental regressions and instill a sense of safety when refactoring.

Leave a Reply