Will Soares Software Developer at Codeminer42

Enzyme vs. react-testing-library: A mindset shift

6 min read 1767

The React logo.

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 availble in the React community.

For this post, we’ll be going over two of those tools: Enzyme and react-testing-library.

While Enzyme has been around for a long time now (released back in 2015), react-testing-library is fairly new in the testing world (released in 2018) but has gained a lot of traction in the last year, which was noted in the last State of JavaScript Survey.

React-testing and other tool usage compared on 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.

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

For the two tools mentioned, 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.

Our examples

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 CodeSandbox 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 you need for each tool. Additionally, you can check the Github repository of each tool (linked above).

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.

The class component looks something like this:

class RangeCounterA 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 codesandbox project linked above for the entire component code.

The functional component will look like this:

const RangeCounterB = 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).

What needs to be tested?

We’ll be testing a few scenarios for both components with both tools. They are:

  • 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

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

describe("RangeCounterA", () => {
  let component;  
  beforeEach(() => {
    component = mount(<RangeCounterA />);
  });

  describe("when incrementing counter is allowed", () => {
    it("updates counter value correctly", () => {
      component.instance().incrementCounter();
      expect(component.state().counter).toEqual(1);
      expect(component.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).

Now let’s look at that same test scenario but with react-testing-library:

describe("RangeCounterB", () => {
  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 done by getting the actual DOM element and checking its content, which represents what the user actually sees.

The following three scenarios show you 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.

With Enzyme:

describe("RangeCounterA", () => {
  let component;
  beforeEach(() => {
    component = mount(<RangeCounterA />);
  });

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

With react-testing-library:

describe("RangeCounterB", () => {
  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.

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 would 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 difficult 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 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

Leave a Reply