DEV Community

loading...
Cover image for Comparing Enzyme with React Testing Library

Comparing Enzyme with React Testing Library

adamgoth profile image Adam Goth ・8 min read

Enzyme has long been a popular library for testing React applications. More recently, React Testing Library has been gaining traction in Enzyme's place. In this post, we'll take a look at how the two compare.

Overview

Enzyme is a JavaScript Testing utility for React that makes it easier to assert, manipulate, and traverse your React Components’ output. It was created by AirBnB and released in 2015. When using Enzyme, it is common to render the React component that you are testing and then test the component based on certain props or state that are passed in, or by calling functions contained within the component.

While Enzyme tests typically focus on components working correctly internally, React Testing Library is more focused on testing the React application as it is experienced by the user. Tests tend to be more focused on the state of the DOM after imitating user behavior rather than the state of a particular component or implementation.

To get a better understanding of this, let's look at some code.

Setting up

In order to compare these two testing libraries, I've created two separate repos. Both projects contain the same exact application (a to-do list, of course). The only difference is that one test file is written using Enzyme and the other is written using React Testing Library. You can easily follow along in this post without running the application, but if you are interested, both repos are available on GitHub.

Repo for testing with Enzyme

Repo for testing with React Testing Library

The file we're going to focus on in both repos is src/components/ToDo.test.js.

Below is our testing file, written in the typical style of Enzyme.

// testing-with-enzyme/src/components/ToDo.test.js

import React from "react"
import { mount } from "enzyme"
import ToDo from "./ToDo"

const setup = () => mount(<ToDo />)

describe("<ToDo/>", () => {
  describe("The default UI", () => {
    it("Renders two default todo items", () => {
      const app = setup()
      expect(app.find(".ToDoItem").length).toBe(2)
    })

    it("Has an input field", () => {
      const app = setup()
      expect(app.find(".ToDoInput").length).toEqual(1)
    })

    it("Has an add button", () => {
      const app = setup()
      expect(app.find(".ToDo-Add").length).toEqual(1)
    })
  })

  describe("Adding items", () => {
    window.alert = jest.fn()
    it("When the add button is pressed, if the input field is empty, prevent item from being added", () => {
      const app = setup()
      app.find(".ToDo-Add").simulate("click")
      expect(app.find(".ToDoItem").length).toBe(2)
    })

    it("When the add button is pressed, if the input field is empty, prevent item from being added", () => {
      const app = setup()
      app.find(".ToDo-Add").simulate("click")
      expect(window.alert).toHaveBeenCalled()
    })

    it("When the add button is pressed, if the input field has text, it creates a new todo item", () => {
      const app = setup()
      const event = { target: { value: "Create more tests" } }
      app.find("input").simulate("change", event)
      app.find(".ToDo-Add").simulate("click")
      expect(
        app
          .find(".ToDoItem-Text")
          .at(2)
          .text()
      ).toEqual("Create more tests")
    })
  })

  describe("Deleting items", () => {
    it("When the delete button is pressed for the first todo item, it removes the entire item", () => {
      const app = setup()
      app
        .find(".ToDoItem-Delete")
        .first()
        .simulate("click")
      expect(app.find(".ToDoItem").length).toBe(1)
    })
  })
})
Enter fullscreen mode Exit fullscreen mode

And then, the same tests, written with React Testing Library.

// testing-with-react-testing-library/src/components/ToDo.test.js

import React from "react"
import { render } from "@testing-library/react"
import userEvent from "@testing-library/user-event"
import ToDo from "./ToDo"

const setup = () => render(<ToDo />)

describe("<ToDo/>", () => {
  describe("The default UI", () => {
    it("Renders two default todo items", () => {
      const { getAllByRole } = setup()
      expect(getAllByRole("listitem").length).toBe(2)
    })

    it("Has an input field", () => {
      const { getByRole } = setup()
      expect(getByRole("textbox")).toBeInTheDocument()
    })

    it("Has an add button", () => {
      const { getByLabelText } = setup()
      expect(getByLabelText("add")).toBeInTheDocument()
    })
  })

  describe("Adding items", () => {
    it("When the add button is pressed, if the input field is empty, prevent item from being added", () => {
      const { getByLabelText } = setup()
      window.alert = jest.fn()
      userEvent.click(getByLabelText("add"))
      expect(window.alert).toHaveBeenCalled()
    })

    it("When the add button is pressed, if the input field has text, it creates a new todo item", async () => {
      const { getByRole, getByLabelText, getByText } = setup()
      const toDoItem = "fake item"
      userEvent.type(getByRole("textbox"), toDoItem)
      userEvent.click(getByLabelText("add"))
      const item = await getByText(toDoItem)
      expect(item).toBeInTheDocument()
    })
  })

  describe("Deleting items", () => {
    it("When the delete button is pressed for the first todo item, it removes the entire item", async () => {
      const { getAllByRole, getByLabelText, queryByText } = setup()
      // default item
      const toDoItem = "clean the house"
      userEvent.click(getByLabelText(`delete ${toDoItem}`))
      const item = await queryByText(toDoItem)
      expect(item).toBeNull()
      // should only be 1 item left
      expect(getAllByRole("listitem").length).toBe(1)
    })
  })
})
Enter fullscreen mode Exit fullscreen mode

Both files test for the following:

  • Renders two default todo items
  • Has an input field
  • Has an add button
  • When the add button is pressed, if the input field is empty, prevent item from being added
  • When the add button is pressed, if the input field has text, it creates a new todo item
  • When the delete button is pressed for the first todo item, it removes the entire item

Because we are using Enzyme's mount function, the components in both tests are rendered similarly, with an instance of the component being created and then attached to the actual DOM. This would not be true if we had used another popular Enzyme function, shallow to render our component. This post does not focus on that difference, but you can read more about the difference here.

The first significant way in that the tests start to differ is when we go to search for a particular element in the DOM to assert its existence or its state. Typically in an Enzyme test, you'll see elements searched for by their classname, as follows:

it("Renders two default todo items", () => {
  const app = setup()
  expect(app.find(".ToDoItem").length).toBe(2)
})
Enter fullscreen mode Exit fullscreen mode

When writing the same test using React Testing Library, you'll notice that we instead use a method called getAllByRole, and pass it an ARIA role of listitem.

it("Renders two default todo items", () => {
  const { getAllByRole } = setup()
  expect(getAllByRole("listitem").length).toBe(2)
})
Enter fullscreen mode Exit fullscreen mode

So why is one better than the other? While classnames are rather arbitrary, ARIA roles are not. ARIA roles provide additional context to elements for accessibility purposes. In the future, as developers, we may go and update our classname. We may tweak the name, we may change the style, we may entirely change how we wrote our CSS. If that happens, all of the sudden our test breaks. But the application has not broken. By querying by an element's role rather than its classname, we can ensure that we are testing the application by looking for elements in the same manner that a user with assistive technology may be looking at the application. We look for elements based on the purpose they convey to our users.

This concept is discussed in the React Testing Library docs, Which query should I use?, which provides recommendations for the order of priority in which you should query for elements. For example, if we can't find an element by its role, our next best bet is to look for a label. Why? Well, that's most likely what our users would do to find a certain part of the application. This highlights React Testing Library's guiding principles.

The more your tests resemble the way your software is used, the more confidence they can give you.

The library is written to provide methods and utilities that encourage you to write tests that closely resemble how your web pages are used. It purposely drives the user towards accessibility and away from testing implementation details.

Let's move on to another example and take a look at the difference in how we test that our application successfully creates a new item in the to-do list.

With Enzyme, it's common to manually create DOM events and then pass them to Enzyme's simulate function, telling it to simulate the change event with this event data that we've created. Below is an example of this.

// testing-with-enzyme/src/components/ToDo.test.js

it("When the add button is pressed, if the input field has text, it creates a new todo item", () => {
  const app = setup()
  const event = { target: { value: "Create more tests" } }
  app.find("input").simulate("change", event)
  app.find(".ToDo-Add").simulate("click")
  expect(
    app
      .find(".ToDoItem-Text")
      .at(2)
      .text()
  ).toEqual("Create more tests")
})
Enter fullscreen mode Exit fullscreen mode

While this does what we'd expect to, it doesn't test the application in the same manner the user would use it. There is a lot of API and implementation information we need to know in order for the test to work. We need to know what the event should look like. We need to know which event API to simulate. We need to know the classname of the element we want to click. We need to know the classname of the new list item to look for is. And lastly, we need to know what order the element should be in so we can compare the text. None of these things are things the user actually knows or cares about. All they know is that when they type in the box and then click the add button, a new item is added to the list.

To get away from testing our code implementation and get closer to testing how the application is actually used, we turn once again to React Testing Library. Instead of creating fake DOM event objects and simulating various change events, we have the ability to mimic how users would actually interact with the application using userEvent's, which are provided by the user-event library.

user-event tries to simulate the real events that would happen in the browser as the user interacts with it. For example userEvent.click(checkbox) would change the state of the checkbox.

Using this, the same test written in React Testing Library looks as follows:

// testing-with-react-testing-library/src/components/ToDo.test.js

it("When the add button is pressed, if the input field has text, it creates a new todo item", async () => {
  const { getByRole, getByLabelText, getByText } = setup()
  const toDoItem = "fake item"
  userEvent.type(getByRole("textbox"), toDoItem)
  userEvent.click(getByLabelText("add"))
  const item = await getByText(toDoItem)
  expect(item).toBeInTheDocument()
})
Enter fullscreen mode Exit fullscreen mode

In contrast to the Enzyme test, to write the React Testing Library test, we don't need to know much more than what the user would now. We first look for an element with the role of textbox, we then simulate the user typing by using userEvent.type, we simulate the user clicking with userEvent.click on the element with the accessibility label of add. We then assert that the text we typed in is appearing in the document.

In addition to being a much closer representation of the user's experience with the application, writing this test this way also makes for a much less brittle test. We could update classnames or change the number of items in the list and the test would still pass because the application would still be working. The same can not be said for the first test written in Enzyme.

Wrapping up

These examples are shown to attempt to highlight some of the benefits that React Testing Library offers and how it differs from the more traditional testing library of Enzyme. Everything React Testing Library offers always comes back to its guiding principle.

The more your tests resemble the way your software is used, the more confidence they can give you.

We've all been there before when a little tiny change to a component causes a test to break without actually breaking any functionality. React Testing Library, used properly, guides us away from writing these types of implementation tests and towards writing more accessible code and more robust tests that more closely resemble how the application is used.

While this post is intended to serve as a high-level introduction to React Testing Library and it's baked-in philosophy, it only scratches the surface of all the library has to offer. To learn more, visit the project's site at testing-library.com.

If you enjoyed this post or found it useful, please consider sharing it on Twitter.

If you want to stay updated on new posts, follow me on Twitter.

If you have any questions, comments, or just want to say hello, send me a message.

Thanks for reading!

Discussion (0)

pic
Editor guide