React Testing Library (RTL)

Unit testing react components with RTL & Jest

Published: 03 August 2020

unit testing react testing library react jest 

Simple Tests


Let's say we have a simple form component like this:

//myComponent.js
import React, { useState } from "react"

const MyComponent = ({ title, textBody }) => {
  const [email, setEmail] = useState("")

  const handleChange = e => setEmail(e.target.value)

  const handleSubmit = e => {
    e.preventDefault()
    return email
  }

  return (
    <form onSubmit={e => handleSubmit(e)}>
      <h1>{title}</h1>
      <p>{textBody}</p>

      <label htmlFor="email">Type your email</label>
      <input
        type="text"
        id="email"
        onChange={e => handleChange(e)}
        value={email}
      />

      <button type="submit">Submit</button>
    </form>
  )
}

export default MyComponent

Let's start writing some basic tests for this:

Each test file will have atleast 4 basic import statements:

//myComponent.test.js

// always need to import React first
import React from "react"

// all methods relating to RTL will be imported here
import { render } from "@testing-library/react"

// jes-dom allows us to have more ways of asserting against the data
import "@testing-library/jest-dom/extend-expect"

//import the component to test
import MyComp from "."

describe("My Component", () => {
  it("dummy test", () => {
    expect(true).toBe(true)
  })
})

Now, we will render our component with some basic props first:

describe("My Component", () => {
  it("renders correctly", () => {
    const { getByText } = render(
      <MyComp title="RTL" textBody="Testing 123455" />
    )
  })
})

Couple of things to note here:

  • render takes in a JSX component with all its props declared
  • render then returns an object which contains many helpful queries (getBy, queryBy, getAllBy, queryAllBy, etc) to access DOM
  • each query can be used with multiple attributes like text, role, etc
  • in our case we have combined getBy + Text option, lets use this to make some assertions
it("renders correctly", () => {
  const { getByText } = render(<MyComp title="RTL" textBody="Testing 123455" />)
  const customHeading = getByText("RTL")
  const customText = getByText("Testing 123455")

  expect(customHeading).toBeInTheDocument()
  expect(customText).toBeInTheDocument()
})

toBeInTheDocument is a matcher which is provided by the jest-dom import we made earlier into our test file.

When using getByText, you can also use a regular expression just in case if you don't want to type out a long string:

expect(getByText(/testing/i)).toBeInTheDocument()

However, if your text is broken up into child nodes like below:

<p>To get the latest info, <a href="#">click here</a></p>

Using getByText('To get the latest info, click here') won't work. You will either need to query both elements separately or use a function:

expect(
  getByText((content, element) => content.includes("click here"))
).toBeInTheDocument()

// where,
// content --> represents the string encountered in the component that was rendered (use any string methods in JS)
// element --> represents the element that's currently being looped over (use any HTML methods like innerHTML)

Targetting Inputs


To target inputs, we have few different ways:

//get back the input to which label is attached
expect(getByLabelText("Type your email")).toBeInTheDocument()

// can also use placeholder text

Firing Events


To fire events like change and click we import the fireEvent prop and then tag on the event name:

import { render, fireEvent } from "@testing-library/react"

it("fires change event", () => {
  const { getByLabelText } = render(
    <MyComp title="RTL" textBody="Testing 123455" />
  )

  fireEvent.change(getByLabelText("Type your email"), {
    target: { value: "lol" },
  })
  expect(getByLabelText("Type your email").value).toBe("lol")
})

The second argument is usually setting up additional properties relating to the event. For the case of click:

// to fire simple left click
fireEvent.click(getByText("Submit"));

// to fire a right click
fireEvent.click(getByText("Submit"),{button:2})
})

Negative Assertions


Sometimes, its also handy to assert that a certain element is not in the DOM.

We can't use getBy queries since they always expect the element to be found or they throw an error.

We use queryBy instead since it will return null if nothing found like so:

expect(queryByText("an element which does not exist")).not.toBeInTheDocument()

Notice, we can preface any query with .not. to indicate the inverse effect.

Targetting SVGs and Images


Images must always an alt attribute associated with them and/or will have roles attached and so we can use getByRole or getByAltText to access them.

Mostly, SVGs will have one of the following 2 properties to be ARIA compliant:

(1) role=img --> use getByRole

(2) title --> use getByTitle

Targetting by testid


In the rare case when you have no way of accessing the element, you can add a data-testid attribute as a last resort to access it with getByTestId query.

<p data-testid="unreachable"></p>
expect(getByTestId("unreachable")).toBeInTheDocument()

This is a last resort because it should never happen if you follow ARIA compliant rules. You should always have some meaningful ways of targeting your nodes/element as discussed above.


React Testing Library - Queries Reference

RTL Firing Events