Migrating from Enzyme to React Testing Library

Mark Waters
John Lewis Partnership Software Engineering
7 min readApr 26, 2022

Hi, I’m Mark, a Front-end developer working on the React stack at John Lewis. I have recently been involved in modernising the testing approach for React components on some of our micro frontend applications.

Introduction

Testing is an essential part of modern web app development, the dynamic and interactive nature of web applications means that validating our code and user journeys is more important than ever. At John Lewis we write unit tests to test Javascript logic, typically with Jest. We use component tests to test higher level React components and write integration tests for more complex user journeys or use cases, typically with Cypress.

Enzyme and React Testing Library (RTL) are packages which provide utilities to enhance unit tests for React components. Enzyme has been popular for a number of years but React Testing Library, a relative newcomer on the block, has been making waves in the React ecosystem. I’m going to talk about why developers are making the switch and how teams can migrate to RTL.

Why component tests?

A pure Javascript function has an input and an output, making it easy to write test cases for the expected inputs and outputs. As we start interacting with browser APIs and using frameworks to generate blocks of HTML it becomes harder to simply think in terms of input/output and we must think more about how the component behaves for a user. Component tests are typically written to match components written in our framework of choice (React in our case). Component tests help us validate that user interaction with UI components matches the behaviour we have written in code.

Testing with Enzyme

Enzyme can shallow render (no child components) or fully render a component (rendering all child components), it also exposes an api to interact with UI components and simulate user events such as button clicks. Developers can then use expectations to assert the behaviour of the component, see the full Enzyme API here. This is a short example test which finds a button based on the class ‘.nav-button’ , clicks the button and checks if our function has been called.

import { shallow } from ‘enzyme’;
test(‘navClickEvent called when NavButton is clicked’, () => { const navClickEvent = jest.fn(); component = shallow(<NavButton navClickEvent={navClickEvent} />); component.find(‘.nav-button’).simulate(‘click’); expect(navClickEvent).toHaveBeenCalledTimes(1);});

Enzyme provides full access to the React component, including props and state, so developers are able to set and read component internals inside tests. To demonstrate this we can pass a function to our component and call it by accessing the props of our component directly.

import { shallow } from ‘enzyme’;
test(‘call function via props example’, () => { const handleClick = jest.fn(); const component = shallow(<ExampleComponent handleClick={handleClick}/>); component.find(‘ExampleComponent’).props().handleClick(); expect(handleClick).toBeCalledTimes(1);});

By doing this we have broken 2 rules of effective component testing 1) we are not simulating a real user’s behaviour and 2) we are accessing internals of the component to artificially trigger the handleClick function. Because Enzyme exposes component internals it is common for developers to adopt testing patterns which depend on them. Tests that manipulate or interact with the internals of a component often result in:

  1. Tests that are written more from the perspective of a developer than a user. A user would have to interact with the component through the UI and wouldn’t be able to directly call a function passed via props. Therefore the test is not a real representation of how the component will behave as a result of user interaction.
  2. Brittle tests, due to interaction with internal component methods and properties. If we are constantly referring to specific prop and method names within our component we have to update our tests every time we rename something, not just when the component logic changes. Tests require more maintenance as a result and will break easily when code is modified or refactored.

The logical next step then is to shift the ethos of the framework from being about nitty gritty component details and towards an interrogation of the DOM after our component has been rendered. This is the ethos that React Testing Library adopts, it aims to provide a more real world environment to test our components.

Testing with React Testing Library (RTL)

RTL is an implementation of the DOM Testing Library which promotes “querying the DOM for nodes in a way that’s similar to how the user finds elements on the page”. The approach of this library is to encourage testing as if you were a user by interacting with the DOM and therefore better simulate the real world usage of a component.

RTL renders components and child components using a simulated DOM environment called JSDOM. In the example below, we render the AccountLinks component but only write tests against the DOM tree which is exposed via the React Testing Library API. We do not run our tests against the component itself or have access to props, or be able to trigger any functions on the component directly. RTL provides the screen object which has various methods to interrogate the DOM.

import { render, screen } from ‘@testing-library/react’;
import userEvent from ‘@testing-library/user-event’;
test(‘account links are displayed when user is signed in’, () => { render(<AccountLinks signedIn={true} />); userEvent.click(screen.getByText(‘Account’)); expect(screen.getByRole(‘link’, { name: /my account/i})).toBeInTheDocument(); expect(screen.getByRole(‘link’, { name: /wish list/i})).toBeInTheDocument(); expect(screen.getByText(“Sign out”)).toBeInTheDocument(); expect(screen.getByText(“Sign in”)).not.toBeInTheDocument();});

This philosophy ensures that we don’t interact with the internals of our component in our tests, we only interact with a version of the DOM after our component has been rendered.

The React Testing Library API encourages testing of the DOM with methods like getByText (which looks for a text string in the DOM), getByRole (which looks for specific HTML role value) and getByTestID (ideally a last resort, select element based on ‘data-testid’ which we assign the HTML element). Similar to Enzyme we still have access to user events and can trigger events on the page, in the example below we use userEvent.click() to do this and pass in the element we want to click.

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
test(‘calls onClose when clicked’, () => { const onClose = jest.fn(); render(<CloseButton onClose={onClose} />); const closeButton = screen.getByRole(‘button’); expect(closeButton).toBeInTheDocument(); userEvent.click(closeButton); expect(onClose).toHaveBeenCalledTimes(1);});

The userEvent module has various methods to simulate user behaviour such as hovering, clicking, selecting options and more. These events can only be triggered by passing a rendered DOM element to the event methods, this ensures a degree of separation between our component and the user actions being performed.

Benefits of React Testing Library

Enzyme is still a popular choice for component tests but it can encourage testing practices which lead to very developer centric and brittle tests. We’ve explored many benefits of RTL already, here’s a summary of the great reasons to make the move to RTL:

  • RTL promotes user-like interactions in component tests.
  • Encourages interrogation of the DOM using text, roles and test id’s (e.g. there is no RTL method to get by class name as these are implementation details), we test what the user experiences when the page is rendered.
  • Promotes stable and meaningful tests by hiding implementation details, whereas Enzyme allows access to component internals with methods like props().
  • The syntax is clean and straightforward.
  • Great documentation.
  • Enzyme’s shallow render does not trigger React hooks such as useEfffect, so workarounds or other libraries are needed if you are using a functional component with hooks.
  • An Enzyme react adapter allows compatibility with React 17 but support for future React versions looks uncertain.

If you’re still unsure about the benefits of RTL I’d recommend this article by RTL creator Kent C Dodds for a detailed overview of its benefits. If your team is thinking about migrating from Enzyme tests to Testing Library check out the migration guide here.

How to make the move in your team:

You can install and use React Testing Library alongside Enzyme, so if you already have a suite of Enzyme tests you can easily create new tests in RTL and keep your existing Enzyme tests. Making a change to your tooling is often a big decision but RTL is very developer friendly, so the best way is simply to get cracking and write some tests using RTL!

Here are some approaches that have worked at John Lewis:

  • Adopt a team policy whereby any new component tests are exclusively written using RTL rather than Enzyme, this is a great starting point in most cases.
  • When a developer touches an existing component or piece of functionality, switch the component tests across to RTL if it is practical to do so.
  • Identify areas in your app that are lacking component tests and use it as an opportunity to bolster your tests with RTL. This approach is particularly appropriate for apps with low test coverage and older apps that use outdated libraries or heavy use of snapshot testing
  • If you have a standard project for new front end apps (we have a NextJS project called Blueprint) bundle RTL with that project and encourage its usage. That way new teams will be encouraged to use it from day 1.

Selling RTL to Your Team

There can occasionally be pushback from other team members when migrating to a new tool. A great way to get other developers on board is run a spike and convert a whole test file to RTL, if you can mob on this even better. This file will give a focal point for the team to discuss what they like and what they are concerned about. Ideally this file will reflect the standard of testing your team is aiming for across its test suite, the ‘gold standard’ if you like. When developers come to pick up their first bit of RTL they have something to refer to and an understanding of how the team has decided to write their tests.

It’s important to sell the benefits of a change like this to other members of the team (such as your Product Owner and Delivery Lead). Migrating to RTL should hopefully save you time in the long run (compared to writing equivalent Enzyme tests) and result in component tests you can really trust. Therefore the effort put in is often reaped by spending less time writing and maintaining tests, something that’s important to communicate to your team.

I hope you’ve found this article useful and best of luck making the transition to RTL. John Lewis are recruiting Front-end developers to work on our React stack, please see our engineering jobs page for more information.

Useful Articles:

--

--