DEV Community

João Forja 💭
João Forja 💭

Posted on • Edited on • Originally published at joaoforja.com

12 Recipes for testing React applications using Testing Library

If you're not sure how to test a specific part of your React application, this article might be useful to you. Although you probably won't a direct answer that you can copy and past, by understanding how the examples work, you'll be able to adapt them to your specific situation.

Note: Keep in mind that the components or hooks under test aren't production-ready and that their only purpose is to illustrate how to test a particular behavior.

Table of Contents

  1. Invokes given callback
  2. Changes current route
  3. High Order Component
  4. Component cleans up on unmount
  5. Depends on Context Provider
  6. Uses functions that depend on time
  7. Custom hooks
  8. Portal
  9. Focus is on correct element
  10. Order of elements
  11. Selected Option
  12. Dynamic page titles
  13. Other resources

Invokes given callback

  • We're testing that after some interaction the component calls a given callback.
  • We give a mock function to the component under test and interact with it so that it calls the callback. Then we assert we called the function with the expected parameters. If relevant, we also check the number of times the function was called.
import React from "react"
import { render, screen } from "@testing-library/react"
import userEvent from "@testing-library/user-event"

test("callback is called on button click", function test() {
  const callback = jest.fn()
  render(<Button action={callback} />)

  userEvent.click(screen.getByRole("button", { name: /call/i }))

  expect(callback).toHaveBeenCalledTimes(1)
  expect(callback).toHaveBeenCalledWith()
})

function Button({ action }) {
  return <button onClick={() => action()}>Call</button>
}
Enter fullscreen mode Exit fullscreen mode

Changes current route

  • We're testing that the component redirects the user to an expected router with the expected query parameters after an interaction.
  • We first create a routing environment similar to that in which we'll use the component. We set up that environment so we can capture the URL to which the component will redirect us. We interact with the component to cause the redirect. We then assert that we were redirected to the URL we expected.
import React, { useState } from "react"
import { render, screen } from "@testing-library/react"
import userEvent from "@testing-library/user-event"
import { MemoryRouter, Route, useHistory } from "react-router-dom"

test("On search redirects to new route", async function test() {
  let location
  render(
    <MemoryRouter initialEntries={["/"]}>
      <Route path="/">
        <SearchBar />
      </Route>
      <Route
        path="/*"
        render={({ location: loc }) => {
          location = loc
          return null
        }}
      />
    </MemoryRouter>
  )

  await userEvent.type(screen.getByLabelText(/search/i), "react")
  userEvent.click(screen.getByRole("button", { name: /submit/i }))

  expect(location.pathname).toEqual("/search-results")
  const searchParams = new URLSearchParams(location.search)
  expect(searchParams.has("query")).toBe(true)
  expect(searchParams.get("query")).toBe("react")
})

function SearchBar() {
  const history = useHistory()
  const [query, setQuery] = useState("")

  return (
    <form
      onSubmit={function redirectToResultsPage(e) {
        e.preventDefault()
        history.push(`/search-results?query=${query}`)
      }}
    >
      <label htmlFor="query">search</label>
      <input
        type="text"
        value={query}
        onChange={e => setQuery(e.currentTarget.value)}
        id="query"
      />
      <input type="submit" value="go" />
    </form>
  )
}
Enter fullscreen mode Exit fullscreen mode

High Order Component

  • We're testing that a HOC gives the props we expect to the wrapped component.
  • We first create a mock component for the HOC to wrap. The mock component will store the received props in a variable. After rendering the component returned by the HOC we assert that it gave the mock component the props we expected.
import React from "react"
import { render } from "@testing-library/react"

test("Adds number and gives result as a prop", function test() {
  let result
  function WrappedComponent({ sum }) {
    result = sum
    return null
  }
  const ComponentWithSum = withSum(WrappedComponent, [4, 6])
  render(<ComponentWithSum />)

  expect(result).toBe(10)
})

function withSum(WrappedComponent, numbersToSum) {
  const sum = numbersToSum.reduce((a, b) => a + b, 0)
  return () => <WrappedComponent sum={sum} />
}
Enter fullscreen mode Exit fullscreen mode

Component cleans up on unmount

  • We want to assert that a component subscribes after mount and unsubscribes after unmount.
  • We start by mocking the subscription methods so we can assert they get called. We then render the component and assert that it subscribed. All that's left to do is make the component unmount and assert it unsubscribed.
import React, { useEffect } from "react"
import { render } from "@testing-library/react"

test("Subscribes and unsubscribes when appropriate", function test() {
  const subscriptionService = {
    subscribe: jest.fn(),
    unsubscribe: jest.fn(),
  }

  const { unmount } = render(
    <ComponentThatSubscribes subscriptionService={subscriptionService} />
  )

  expect(subscriptionService.subscribe).toHaveBeenCalledTimes(1)
  expect(subscriptionService.subscribe).toHaveBeenCalledWith()

  unmount()

  expect(subscriptionService.unsubscribe).toHaveBeenCalledTimes(1)
  expect(subscriptionService.unsubscribe).toHaveBeenCalledWith()
})

function ComponentThatSubscribes({ subscriptionService }) {
  useEffect(() => {
    subscriptionService.subscribe()
    return () => subscriptionService.unsubscribe()
  }, [subscriptionService])
  return null
}
Enter fullscreen mode Exit fullscreen mode

Depends on Context Provider

  • We want to test a component that depends on a context Provider
  • To test the component, we'll recreate the environment in which we'll use the component. In other words, we'll wrap the component in the Context Provider.
import React, { useContext } from "react"
import { render, screen } from "@testing-library/react"

test("displays name of current user", function test() {
  render(
    <UserContext.Provider value={{ user: { fullName: "Giorno Giovanna" } }}>
      <UserFullName />
    </UserContext.Provider>
  )
  expect(screen.getByText("Giorno Giovanna")).toBeVisible()
})

const UserContext = React.createContext()

function UserFullName() {
  const { user } = useContext(UserContext)
  return <p>{user.fullName}</p>
}
Enter fullscreen mode Exit fullscreen mode

Uses functions that depend on time

  • We want to test a component that depends on real-time. In this example, that dependency comes from using setTimeout().
  • When testing components that depend on real-time, we need to be aware that those tests shouldn't take too long. One way to do that is to have the component receive the time interval as a prop to allow us to configure a shorter time interval for tests than we would have in production.
import React, { useState, useEffect } from "react"
import {
  render,
  screen,
  waitForElementToBeRemoved,
} from "@testing-library/react"

test("Changes from red to green to after timeout", async function test() {
  render(<TrafficLight timeUntilChange={10} />)

  expect(screen.getByText(/red/i)).toBeVisible()
  await waitForElementToBeRemoved(() => screen.getByText(/red/i))
  expect(screen.getByText(/green/i)).toBeVisible()
})

function TrafficLight({ timeUntilChange = 500 }) {
  const [light, setLight] = useState("Red")
  useEffect(() => {
    setTimeout(() => setLight("Green"), timeUntilChange)
  }, [timeUntilChange])
  return <p>{light}</p>
}
Enter fullscreen mode Exit fullscreen mode

Custom hooks

  • We want to test a custom hook.
  • Since we're testing a hook, we'll need to call it inside a component otherwise we'll get an error. Therefore, we'll create a mock component, use the hook inside it, and store what the hook returns in a variable. Now we can assert what we need to assert using that variable.
import React, { useState, useCallback } from "react"
import { render, act } from "@testing-library/react"

test("counter increments", function test() {
  let counter
  function MockComponent() {
    counter = useCounter()
    return null
  }

  render(<MockComponent />)

  expect(counter.count).toEqual(0)
  act(() => counter.increment())
  expect(counter.count).toEqual(1)
})

function useCounter() {
  const [count, setCount] = useState(0)
  const increment = useCallback(() => setCount(x => x + 1), [])
  return { count, increment }
}
Enter fullscreen mode Exit fullscreen mode
import React, { useState, useCallback } from "react"
import { renderHook, act } from "@testing-library/react-hooks"

test("counter increments with react hooks testing library", function test() {
  const { result } = renderHook(() => useCounter())
  expect(result.current.count).toBe(0)
  act(() => result.current.increment())
  expect(result.current.count).toBe(1)
})

function useCounter() {
  const [count, setCount] = useState(0)
  const increment = useCallback(() => setCount(x => x + 1), [])
  return { count, increment }
}
Enter fullscreen mode Exit fullscreen mode
  • If you're looking for more examples on how to test react hooks, I recommend you take a look at the usage section of the react hooks testing library documentation. They have excellent documentation on how to deal with other use cases like errors and asynchronous updates.

Portal

  • We want to test a component that's a portal.
  • A portal needs a DOM node to be rendered into. So to test it, we'll have to create that DOM node. After we make the assertions, we'll have to remove the DOM node as not to affect other tests.
import React, { useRef, useEffect, useState } from "react"
import ReactDOM from "react-dom"
import { render, screen } from "@testing-library/react"
import userEvent from "@testing-library/user-event"

test("PortalCounter starts at 0 and increments", function test() {
  const modalRoot = document.createElement("div")
  modalRoot.setAttribute("id", "modal-root")
  document.body.appendChild(modalRoot)

  render(<PortalCounter />)

  expect(screen.getByTestId("counter")).toHaveTextContent("0")
  userEvent.click(screen.getByRole("button", { name: "inc" }))
  expect(screen.getByTestId("counter")).toHaveTextContent("1")

  document.body.removeChild(modalRoot)
})

function PortalCounter() {
  const el = useRef(document.createElement("div"))
  const [count, setCount] = useState(0)

  useEffect(() => {
    const modalRoot = document.getElementById("modal-root")
    const currentEl = el.current
    modalRoot.appendChild(currentEl)
    return () => modalRoot.removeChild(currentEl)
  }, [])

  return ReactDOM.createPortal(
    <>
      <section aria-live="polite">
        count: <span data-testid="counter">{count}</span>
      </section>
      <button type="button" onClick={() => setCount(c => c + 1)}>
        inc
      </button>
    </>,
    el.current
  )
}
Enter fullscreen mode Exit fullscreen mode

Focus is on correct element

  • We want to test that the focus on the element we expect.
  • We can verify if an element has focus or not by using toHaveFocus().
import React from "react"
import { render } from "@testing-library/react"
import userEvent from "@testing-library/user-event"

test("clicking on label gives focus to name input", () => {
  const { getByText, getByLabelText } = render(<NameForm />)

  const nameLabel = getByText("Name")
  userEvent.click(nameLabel)

  const nameInput = getByLabelText("Name")
  expect(nameInput).toHaveFocus()
})

function NameForm() {
  return (
    <form>
      <label htmlFor="name">Name</label>
      <input id="name" type="text" />
    </form>
  )
}
Enter fullscreen mode Exit fullscreen mode

Order of elements

  • We want to test that a list of elements is rendered in the expected order.
  • We'll take advantage of AllBy queries returning elements in the order in which they appear on the HTML.
  • It's important to note that this approach doesn't take into account CSS that might change the order in which the elements are displayed.
import React from "react"
import { render, screen } from "@testing-library/react"

test("renders names in given order", () => {
  const names = ["Bucciarati", "Abbacchio", "Narancia"]

  render(<NamesList names={names} />)

  const renderedNames = screen.getAllByRole("listitem")
  expect(renderedNames[0]).toHaveTextContent("Bucciarati")
  expect(renderedNames[1]).toHaveTextContent("Abbacchio")
  expect(renderedNames[2]).toHaveTextContent("Narancia")
})

function NamesList({ names }) {
  return (
    <ul>
      {names.map(name => (
        <li key={name}>{name}</li>
      ))}
    </ul>
  )
}
Enter fullscreen mode Exit fullscreen mode

Selected Option

  • We want to test that an input is checked.
  • We can use toBeChecked() to test if an element is checked.
import React from "react"
import { render, screen } from "@testing-library/react"

test("Has Summer pre-selected", function test() {
  render(<SeasonsForm />)
  expect(screen.getByRole("radio", { name: /summer/i })).toBeChecked()
})

function SeasonsForm() {
  return (
    <form>
      <p>Beast season:</p>
      <section>
        <input name="season" type="radio" id="winter" value="winter" />
        <label htmlFor="winter">Winter</label>
        <input name="season" type="radio" id="spring" value="spring" />
        <label htmlFor="spring">Spring</label>
        <input
          name="season"
          checked
          readOnly
          type="radio"
          id="summer"
          value="summer"
        />
        <label htmlFor="summer">Summer</label>
        <input name="season" type="radio" id="autumn" value="autumn" />
        <label htmlFor="autumn">Autumn</label>
      </section>
    </form>
  )
}
Enter fullscreen mode Exit fullscreen mode

Dynamic page titles

  • We want to test that the title of the current page is updated.
  • We access the current title by using document.title. Since the document title won't be immediately updated, we need to wait for the change using waitFor.
import React, { useState } from "react"
import { waitFor, render, screen } from "@testing-library/react"
import userEvent from "@testing-library/user-event"
import { Helmet } from "react-helmet"

test("Increments document title", async function test() {
  render(<DocTitleCounter />)
  await waitFor(() => expect(document.title).toEqual("0"))
  userEvent.click(screen.getByRole("button", { name: /inc/i }))
  return waitFor(() => expect(document.title).toEqual("1"))
})

function DocTitleCounter() {
  const [counter, setCounter] = useState(0)

  return (
    <>
      <Helmet>
        <title>{String(counter)}</title>
      </Helmet>
      <button onClick={() => setCounter(c => c + 1)}>inc</button>;
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

Other resources


If you enjoyed this article you can follow me on twitter where I share my thoughts about software development and life in general.

Top comments (0)