DEV Community

loading...
Cover image for Better Tests for Text Content with React Testing Library

Better Tests for Text Content with React Testing Library

alexkmarshall profile image Alex ・4 min read

Cover Photo by Scott Webb on Unsplash

When testing React apps, there can be many ways to write a test. Yet small changes can make a big difference in readability and effectiveness.

In this post I'm going to explore a common scenario. Testing a component that renders some text based on a variable prop. I'll assume a basic familiarity with React and React Testing Library.

For this example I have a greeting component which accepts a name prop. This renders a welcome message customised with the provided name.

function Greeting({name}) {
  return <h1>Welcome {name}!</h1>
}
Enter fullscreen mode Exit fullscreen mode

Let's test this.

import {render, screen} from '@testing-library/react'
import Greeting from './greeting'

test('it renders the given name in the greeting', () => {
  render(<Greeting name="Jane"/>)
  expect(screen.getByText(`Welcome Jane!`)).toBeInTheDocument()
})
Enter fullscreen mode Exit fullscreen mode

We can write a test like this, and sure enough it passes. Here we're checking that the text we expect renders. But there are a few problems we can try and fix.

  • First off, the name 'Jane' appears twice in our test, we can pull that out into a variable making our test more readable.
  • Second, if we change the component to render a different element rather than a heading, this test will still pass. But that's a change we would like our tests to tell us about.
  • Third, if we break the component, and stop rendering the name, we don't get a great test failure message.

Use Variables in Tests

test('it renders the given name in the greeting', () => {
  const name = 'Jane'
  render(<Greeting name={name}/>)
  expect(screen.getByText(`Welcome ${name}!`)).toBeInTheDocument()
})
Enter fullscreen mode Exit fullscreen mode

Here we extract the name into a variable. It is now clearer that the name is the focus of the test.

We could go even further and use a library like FakerJs to generate a random name. That way we can communicate that the specific name itself is not important, just that the name is rendered.

import faker from 'faker'
test('it renders the given name in the greeting', () => {
  const name = faker.name.firstName()
  render(<Greeting name={name}/>)
  expect(screen.getByText(`Welcome ${name}!`)).toBeInTheDocument()
})
Enter fullscreen mode Exit fullscreen mode

Test for Accessible Elements

Now we can address the element that is being rendered. Instead of only looking for the element by its text, we can check by its role, in this case heading. We provide the text we are looking for as the name property in the optional second argument to getByRole.

expect(
  screen.getByRole('heading', { name: `Welcome ${name}!` }
).toBeInTheDocument()
Enter fullscreen mode Exit fullscreen mode

If we were to change the component to render a div instead of an h1 our test would fail. Our previous version would have still passed, not alerting us to this change. Checks like these are very important to preserve the semantic meaning of our rendered markup.

Test failure with text 'Unable to find an accessible element with the role "heading" and name "Welcome Jane!"'

Improving Test Failure Message

If we break the component, and stop rendering the name, our failure message still isn't ideal.

Similar error message to previous failure, but with a list of accessible roles, including 'heading: Name "Welcome !"'

It's not terrible. Jest gives us the accessible elements that it found, and we can see here that the name is missing. But if this was a larger component it may be time consuming to search through this log to find what's wrong. We can do better.

expect(
  screen.getByRole('heading', { name: /welcome/i }
).toHaveTextContent(`Welcome ${name}!`)
Enter fullscreen mode Exit fullscreen mode

We've done a couple of things here. We've extracted the static part of the text, which in this case is the word 'welcome'. Instead of searching by the full text string, we'll find the heading element that includes /welcome/i. We use a regex here instead of a plain string, so we can do a partial match on just that part of the text.

Next, instead of expecting what we found toBeInTheDocument we can use a different matcher from jest-dom. Using toHaveTextContent checks that the text in the element is what we expect. This is better for two reasons. First, reading the text it communicates that the text content is the thing that we are checking - not only that some element exits. Second, we get a far better test failure message.

Test failure saying 'Expected element to have text content: Welcome Jane!, Received: Welcome !'

Here we see right away what the problem is, we don't have to hunt anywhere to find it.

Recap

  • We have extracted variables in our test to communicate what is important data for our test.
  • We used getByRole to validate the semantics of our component.
  • We used toHaveTextContent to communicate what output our test is checking. And to get more useful test failure messages.

I picked up some of the techniques here from Kent C Dodd's Epic React course. It has supercharged my understanding of all things React, even things I thought I already knew well.

This guide of which query to use with React Testing Library is also very useful. The jest-dom documentation gives you an idea of all the matchers you can use to improve your tests.

Discussion (3)

pic
Editor guide
Collapse
andrewcooke89 profile image
Andrew Cooke

Great post! You should definitely create a few more

Collapse
alexkmarshall profile image
Alex Author

Thanks, I do have a couple more testing-related things planned.

Collapse
snigo profile image
Igor Snitkin

Good read! Thanks for sharing ;)