Better Tests for Text Content with React Testing Library
Tags: react, testing
Published: 2021-03-16 15:01:40

Cover Photo by <a href="https://unsplash.com/@scottwebb?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Scott Webb</a> on <a href="/s/photos/abstract-background?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>
  
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.

```javascript
function Greeting({name}) {
  return <h1>Welcome {name}!</h1>
}
```



Let's test this. 

```javascript
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()
})
```

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

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

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](https://github.com/marak/Faker.js/) to generate a random name. That way we can communicate that the specific name itself is not important, just that the name is rendered.

```javascript
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()
})
```

## 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`.

```javascript
expect(
  screen.getByRole('heading', { name: `Welcome ${name}!` }
).toBeInTheDocument()
```

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!"'](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w95snxrjpjqjadjndh8k.png)

## 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 !"'](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zn1q3ati6e4mn60gfgre.png)

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.

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

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 !'](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cmwp3j6ogh0yf20mjxx8.png)

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](https://epicreact.dev/) 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](https://testing-library.com/docs/queries/about/) is also very useful. The [jest-dom documentation](https://github.com/testing-library/jest-dom) gives you an idea of all the matchers you can use to improve your tests.