Skip Main Navigation

React Testing Library over Enzyme

21 April 2020 · 5 min read

As I continue to use React Testing Library more and more, I’m convinced it’s the way to go for testing React apps and components. When Kent first released React Testing Library and was sharing it out, I thought it was nice. But I was feeling pretty successful in my Enzyme tests so why switch? I thought React Testing Library was more or less the same as Enzyme, just with a narrower API to keep you shooting yourself in the foot. I was kinda right.

When it comes to testing React components there are 3 main phases:

  1. Finding an element in the DOM
  2. Interacting with the element (optional)
  3. Asserting that the UI is in the correct state

Finding

React Testing Library tries to find elements based upon how users see your UI and not how you’ve built it. Let’s take the example from the docs where we have a HiddenMessage component that toggles whether to display the children passed to it:

const HiddenMessage = ({ children }) => {
  const [showMessage, setShowMessage] = React.useState(false)
  return (
    <div>
      <label htmlFor="toggle">Show Message</label>
      <input
        id="toggle"
        type="checkbox"
        onChange={(e) => setShowMessage(e.target.checked)}
        checked={showMessage}
      />
      {showMessage ? children : null}
    </div>
  )
}

In order to write a test to see if the component shows the children when the checkbox is clicked we need to first find the checkbox. With Enzyme I would typically add a data-testid="toggle-message" attribute to the checkbox and search for it like:

import { mount } from 'enzyme'
import HiddenMessage from './HiddenMessage'

test('shows the children when the checkbox is checked', () => {
  const component = mount(<HiddenMessage>Test Message</HiddenMessage>)

  const checkbox = component.find('[data-testid="toggle-message"]')})

NOTE: Searching by element name ('input') or class name makes your test brittle and not resilient to change.

Instead with React Testing Library, we search for the checkbox based on its label, “Show Message”. This is what the user sees and what they click on. So our tests acts more like what our users are doing. The search for the checkbox looks like:

import { render, fireEvent } from '@testing-library/react'
import HiddenMessage from './HiddenMessage'

test('shows the children when the checkbox is checked', () => {
  const { getByLabelText } = render(<HiddenMessage>Test Message</HiddenMessage>)

  const checkbox = getByLabelText('Show Message')})

There’s really no easy way to replicate this in Enzyme without recreating the functionality yourself to search for all <label> tags and then compare against their contents. I really like how getByLabelText pushes us to use a <label> tag, which is great for both UX and Accessibility. The testing library is not only making our code better, but the app itself too!

NOTE: React Testing Library provides *ByLabelText, *ByPlaceholderText, *ByText, *ByAltText, *ByTitle*, *ByDisplayValue, *ByRole, and finally the fallback *ByTestId. Most of these are accessibility-friendly searches! 🙌🏾

Interacting

In the example of our <HiddenMessage /> component, we need to click the checkbox in order to get to the new state. In Enzyme this would look like:

import { mount } from 'enzyme'
import HiddenMessage from './HiddenMessage'

test('shows the children when the checkbox is checked', () => {
  const component = mount(<HiddenMessage>Test Message</HiddenMessage>)

  const checkbox = component.find('[data-testid="toggle-message"]')

  checkbox.simulate('click')})

and in React Testing Library:

import { render, fireEvent } from '@testing-library/react'
import HiddenMessage from './HiddenMessage'

test('shows the children when the checkbox is checked', () => {
  const { getByLabelText } = render(<HiddenMessage>Test Message</HiddenMessage>)

  const checkbox = getByLabelText('Show Message')

  fireEvent.click(checkbox)})

More or less the same right?

NOTE: If your component is display-only, and only configurable by props, there is nothing to interact with in order to get it into a new state. In these cases, there is no Interacting phase.

Asserting

The final phase is to verify that your UI is in the correct state; that certain elements are rendered, that other elements have the correct content, etc. Asserting is usually accomplished with matchers in Jest. You can use the basic matchers that come with Jest, but I really prefer using the extended matchers that come with React Testing Library (@testing-library/jest-dom) or Enzyme (jest-enzyme). When tests fail, these provide much better error messages than true !== false.

Just like with Interacting, the Asserting phase for both React Testing Library and Enzyme are more or less the same as long as you avoid certain matchers. For instance, you shouldn’t use .toHaveState() from jest-enzyme because that is testing implementation details. Instead, you should test the result of that state update which is reflected in updated UI.

Wait, that’s it?

Enzyme feels more lower level, as if React Testing Library would be built on top of it (it isn’t by the way). So even though 2 of the 3 phases are more or less the same between the Enzyme and React Testing Library, I’m finding React Testing Library helps me approach testing differently. I test higher in the component tree to avoid testing implementation details. And it’s that difference that makes all the difference. 😄

By the way, there are certainly more nuances around React Testing Library that help prevent you from testing implementation details. This is certainly by no means exhaustive. But all that is for another blog post. Actually, one has already been written. Read Kent’s blog post on Testing Implementation Details. 🙃

Keep learning my friends. 🤓


Hi, I'm Ben Ilegbodu. 👋🏾

I'm a Christian, husband, and father of 3, with 15+ years of professional experience developing user interfaces for the Web. I'm a Principal Frontend Engineer at Stitch Fix, frontend development teacher, Google Developer Expert, and Microsoft MVP. I love helping developers level up their frontend skills.

Discuss on Twitter // Edit on Github


Attend upcoming minishops