Why I'm using react-testing-library instead of Enzyme

June 14, 2019

Enzyme has been my weapon of choice since 2016 for testing my React components. What I liked the most with Enzyme was the isolation of the component when testing it using shallow rendering. I could focus on testing the component behaviour and checking that the correct props were passed down to the children (mostly using snapshot testing). That allowed me not to have to mock the children components, for instance for root components, and have tests that run quickly. So, I was only testing at the unit level, each component separately, and tested the big picture using Cypress (end to end testing).

It’s only when I started to use React 16.8 (“The One With Hooks”) that I looked at other testing libraries. The main reason was the non-possibility to test custom hooks with Enzyme and not being able to use shallow rendering. Which led me to be more curious with other solutions and to dig into react-testing-library finally.

Issues with Enzyme

When rendering a component using Enzyme, it wraps the component and allows us to traverse the tree and access the component’s data (instance, props, state, children, …) using a rich API. But, great power comes great responsibility and being able to access internal state of the component (.state(), .setState(), .instance()) often leads to test its implementation. I saw it many times when working on a React codebase, sometimes because it’s “easier and faster than having to reproduce the user steps”. For instance, in a component that has buttons I used to search for a Button component and call its onClick prop instead of having to mount and simulate the click on that element.

wrapper
  .find('Button')
  .at(0)
  .prop('onClick')();

But, the test would break if we replace the Button with a component having similar functionalities or if we decide to move the button at a different index. It can get really complicated to work with this type of test, since every time you touch the implementation a test breaks. Which makes refactoring really painful. It’s not Enzyme faults per se, but giving access to the private properties to developers allow them to take shortcuts and test the implementation detail which results in brittle tests.

Different approach

React testing library has a different approach to testing than Enzyme. It’s closer to integration testing than unit testing. It renders the component and attaches it to the DOM. We only have access to the elements that are in the DOM. No internal state, no instance methods, just what the user can interact with. And that’s at the end the most important, we want to make sure that our code won’t break when it gets in the user’s hands. It helped me to rethink how and what I test when working on React components.

So far, it’s been a great experience:

  • I like that there is only one type of rendering, so I don’t need to think about it;
  • Tests are still fast to run. I was in the wrong impression that rendering the component on the DOM between each test would be more expensive;
  • The DOM utils are focused on accessibility (getByRole, getByAltText, etc);
  • I rely less on snapshot testing and test more individual elements (jest-dom is handy for that) so tests break less when I change an unrelated node element;
  • My test structure is very similar to what I used to do with Enzyme using a setup factory.

But, it also comes with some downside like having to mock children dependencies, for instance, if it fetches data on mount. So depending on the need, I might use jest.mock to mock a child component.

import { render } from '@testing-library/react';
import React from 'react';
import App from './App';
import SomeAppSection from './SomeAppSection';

jest.mock('./SomeAppSection');

SomeAppSection.mockReturnValue(<div>SomeAppSection</div>);

const setup = (props = {}) => render(<App logout={jest.fn()} {...props} />);

it('renders SomeAppSection', () => {
  const logout = jest.fn();
  const { getByText } = setup({ logout });

  getByText('SomeAppSection');
  expect(SomeAppSection).toHaveBeenCalledWith({ logout }, {});
});

In this example, I don’t want to have to deal with SomeAppSection behaviour since it will require me to have too many things to mock or keep track of when the component will change in the future. So, I fall back to what I used to do with Enzyme by mocking it away.

Great reads related to testing-library or testing in general:

Feel free to send feedback or ask questions on Twitter.



Written by Antoine Lehurt (a.k.a kewah). Senior front-end engineer living in Stockholm and currently working at Acast.