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>
}
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()
})
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()
})
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()
})
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()
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.
Improving Test Failure Message
If we break the component, and stop rendering the name, our failure message still isn't ideal.
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}!`)
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.
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.
Top comments (4)
Thanks, Please i need more of these
Great post! You should definitely create a few more
Thanks, I do have a couple more testing-related things planned.
Good read! Thanks for sharing ;)