
Hiring? Flexiple helps you build your dream team of developers and designers.
React Testing Library Cheat Sheet
When it comes to testing React apps manually, we can either choose to render individual component trees in a simplified test environment or run the complete app in a realistic browser environment (end-to-end testing). But if we were to do some automated tests, the React team recommends choosing some libraries, one of them being the React Testing Library. But what is it? This React Testing Library Cheat Sheet will answer all your questions!
React Testing Library is a set of helpers built on top of the DOM Testing Library by adding APIs to test React components without relying on their implementation details.
As you can get by its name, this library is specifically built to work with React components, and it has out-of-the-box support if you are using the Create React App toolkit to develop React application.
In this cheat sheet, we will walk you through a majority of its topics suited for beginners to pros. Let's begin!
Basic level - React Testing Library Cheat Sheet
1. What problem is React Testing Library trying to solve, and how does it provide the solution?
The typical problem we see while writing tests for our code is that we need maintainability. Our tests should be maintainable for React components, which involves avoiding our components' implementation details and focusing on making them robust. The testbase should be maintainable in the long run so that the refactoring doesn't break your tests and cause another issue to slow down the development process.
That's what React Testing Library comes in to solve this problem. It's quite a lightweight solution for testing React components providing utility functions on top of react-dom
and react-dom/test-utils
. The best part is that it allows for these functions in a way that encourages better testing practices.
React Testing Library's primary guiding principle is:
The more your tests resemble the way your software is used, the more confidence they can give you.
Here are some of the ways it provides a better testing solution:
-
Your tests work in actual DOM nodes instead of dealing with the instances of rendered React components.
- The utilities facilitate querying the DOM the same way the user would without the fluff.
- It exposes a recommended way to find elements by a `data-testid` as an 'escape-hatch' for elements where the text content and label don't make sense or aren't practical.
- It encourages your applications to be more accessible and makes sure that the app works when a real user uses it.
2. A basic component render test
Let's say we have the following component in our App.js file:
const title = 'Hello, World!';
function App() {
return <div>{title}</div>;
}
export default App;
```
We can have the following test code in the App.test.js file:
```
import { render } from '@testing-library/react';
import App from './App';
describe('App', () => {
test('renders App component', () => {
render(<App />);
});
});
React Testing Library's render
function takes any JSX code to render it. By this, you have access to the React component in your test. To verify this, we can use the library's debug
function like this:
import { render, screen } from '@testing-library/react';
import App from './App';
describe('App', () => {
test('renders App component', () => {
render(<App />);
screen.debug();
});
});
Now in the command line, you should see the HTML output of the App
component as follows:
<body>
<div>
<div>
Hello, World!
</div>
</div>
</body>
3. Why use React Testing Library?
When you get to know about React Testing Library, you might find its resemblance to another popular library, Enzyme. Enzyme is a JavaScript Testing utility for React that makes testing your React Component's output easier.
But React Testing Library differs from Enzyme and other testing libraries due to the following points:
1. The primary purpose of this library is to increase confidence in your tests by testing your components in a way the user would use them in real life. So, instead of accessing the component's internal APIs or evaluating their state, React Testing Library gives us more confidence by writing the tests based on the component output.
2. It aims to solve the problems many developers were facing when writing tests using Enzyme, thereby leading them to write implementation details. As a result, the tests slow down development speed and productivity. With React Testing Library, you don't have to worry about rewriting parts of your tests.
3. It increases developer experience in writing tests as the syntax is very intuitive, and you don't have to know any intricacies in order to get up and running with your tests. You can start querying the DOM using methods like getByText
, getByAltText
etc., like an actual end-user.
4. Queries in React Testing Library
Queries are the methods that the Testing Library gives you to find elements on the page. Some different types of queries you get are "get", "find" and "query" and they differ from each other on the basis of whether the query will throw an error if no element is found or if it will return a Promise and retry.
After the element is selected, you can use the Events API or user-event to fire events and simulate user interactions or even use Jest and jest-dom to make assertions about the element.
Here's an example of a query in React Testing Library:
import { render, screen } from '@testing-library/react'
test('should show login form', () => {
render(<Login />)
const input = screen.getByLabelText('Username')
// Events and assertions...
})
Some of the different types of queries we get out of the box can be grouped into single and multiple elements queries.
Single element queries:
getBy...
: it returns a matching node for a query and throws a descriptive error if no elements match.-
queryBy...
: it returns the matching node for a query and returns nil if no elements match. It's used to assert an element that's not present. -
findBy...
: it returns a Promise which resolves when an element is found that matches the given query. The Promise is rejected if no element is found or more than one element is found.
Multiple elements queries:
getAllBy...
: it returns an array of all matching nodes for a query and throws an error if no elements match.-
queryAllBy...
: it returns an array of all matching nodes for a query and an empty array ([]) if no elements match. -
findAllBy...
: it returns a promise which resolves an array of elements when any elements are found that match the given query. If no elements are found, the Promise is rejected.
5. What levels of component tree to test?
You might have doubts about whether you should test the children, parents, or both of them in the component tree. But it is helpful to break down how tests are organized around how the user experiences and interacts with the app's functionality rather than around specific components themselves.
Sometimes, it might be helpful to test for and test each reusable component individually; other times, the specific breakdown of a component tree is just an implementation detail, and testing every component individually can cause issues.
Hence it's often preferable to test high enough up the component tree to simulate realistic user interactions.
Intermediate level - React Testing Library Cheat Sheet
1. Comparing React Testing Library with Jest
First and foremost, React Testing Library is not an alternative to Jest. React Testing Library needs Jest (and vice versa), each of which has a predetermined straightforward task.
Jest is the most popular testing framework for JavaScript applications. Apart from being a test runner (i.e., you can run it with npm
test once you've set up the test script in your package.json file), Jest offers you the describe-test block
and related functions:
describe('my function or component', () => {
test('does the following', () => {
// Your code
});
});
Here the describe
block is the test suite, and the test block is the test case. A test suite can have multiple test cases. What you put into the test cases are called assertions, with can either be true or false. Here's an example in Jest:
describe('true is truthy and false is falsy', () => {
test('true is truthy', () => {
expect(true).toBe(true);
});
test('false is falsy', () => {
expect(false).toBe(false);
});
});
As you can see, there's nothing about React components yet in Jest test cases. Hence, we call Jest a test runner. It gives you the ability to run the tests with Jest from the command line. Jest also offers functions for test suites, test cases, or assertions. Hence, we need Jest to do all of that work.
Meanwhile, React Testing Library is one of the testing libraries to test React components. It provides virtual DOMs and isn't specific to any testing framework.
2. Breakdown of a test
Let's say we have the following test file that uses Mock Service Worker; the recommended way to declaratively mock API communication in your tests:
// fetch.test.jsx
import React from 'react'
import {rest} from 'msw'
import {setupServer} from 'msw/node'
import {render, fireEvent, waitFor, screen} from '@testing-library/react'
import '@testing-library/jest-dom'
import Fetch from '../fetch'
const server = setupServer(
rest.get('/greeting', (req, res, ctx) => {
return res(ctx.json({greeting: 'hello there'}))
}),
)
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
test('loads and displays greeting', async () => {
render(<Fetch url="/greeting" />)
fireEvent.click(screen.getByText('Load Greeting'))
await waitFor(() => screen.getByRole('heading'))
expect(screen.getByRole('heading')).toHaveTextContent('hello there')
expect(screen.getByRole('button')).toBeDisabled()
})
test('handles server error', async () => {
server.use(
rest.get('/greeting', (req, res, ctx) => {
return res(ctx.status(500))
}),
)
render(<Fetch url="/greeting" />)
fireEvent.click(screen.getByText('Load Greeting'))
await waitFor(() => screen.getByRole('alert'))
expect(screen.getByRole('alert')).toHaveTextContent('Oops, failed to fetch!')
expect(screen.getByRole('button')).not.toBeDisabled()
})
Let's do a breakdown of each line of code step by step:
1. Imports: we need to import the API mocking utilities from Mocker Server Worker (MSW), including the rest
and setupServer
utilities:
import {rest} from 'msw'
import {setupServer} from 'msw/node'
We also import specific react-testing methods from the library/react
package: -
import {render, fireEvent, waitFor, screen} from '@testing-library/react'
Along with that, we need custom Jest matchers from jest-dom
:
import '@testing-library/jest-dom'
2. Mocking up: we use the setupServer
function from msw
to mock an API request that our tested component makes. This happens in the following steps:
- Declaring which API requests to mock:
const server = setupServer( rest.get('/greeting', (req, res, ctx) => { return res(ctx.json({greeting: 'hello there'})) }), )
-
Establishing API mocking before all tests:
beforeAll(() => server.listen()) afterEach(() => server.resetHandlers()) afterAll(() => server.close())
-
Using the test case to override the initial
"Get /greeting"
request handler to return a 500 Server Error:test('handles server error', async () => { server.use( rest.get('/greeting', (req, res, ctx) => { return res(ctx.status(500)) }), ) ... })
- User enters a valid username that has public repos.
- User enters a valid username with no associated public repos.
- User enters a username that doesn't exist on GitHub.
3. Arranging: by using the render
method, we make sure it renders a React element into the DOM:
render(<Fetch url="/greeting" />)
4. Simulating user actions: with the fireEvent
method, it allows you to fire events in order to simulate user actions:
fireEvent.click(screen.getByText('Load Greeting'))
await waitFor(() =>
screen.getByRole('heading'),
)
Here, we wait until the get
request promise resolves and the component calls setState
and re-renders. waitFor
function is used to wait until the callback doesn't throw an error, and getByRole
throws an error if it cannot find an element in DOM.
5. Asserting: here, we assert that the alert message is correct using the toHaveTextContent
matcher from jest-dom
:
expect(screen.getByRole('alert')).toHaveTextContent('Oops, failed to fetch!')
Finally, we also assert that the button isn't disabled using toBeDisabled
, another custom matcher from jest-dom
:
expect(screen.getByRole('button')).not.toBeDisabled()
3. render Options
The render method is used to render into a container appended to doument.body
:
import {render} from '@testing-library/react'
import '@testing-library/jest-dom'
test('renders a message', () => {
const {asFragment, getByText} = render(<Greeting />)
expect(getByText('Hello, world!')).toBeInTheDocument()
expect(asFragment()).toMatchInlineSnapshot(`
<h1>Hello, World!</h1>
`)
})
But there are some extra options available to this method, such as:
1. container
: if you provide your own HTMLElement
container via this option, it will not automatically be appended to the document.body
. For example, if you’re testing a tablebody
element, it can't be a child of a div
, so in this case, you can specify a table
as the render container
:
const table = document.createElement('table')
const {container} = render(<TableBody {...props} />, {
container: document.body.appendChild(table),
})
2. baseElement
: this is used as the element for the queries as well as what is printed when you use debug()
. If container
is specified, it defaults to the same; else, it defaults to document.body
.
3. hydrate
: if this is set to true, then it renders with ReactDOM.hydrate
, which is used to hydrate a container whose HTML contents were rendered by ReactDOMServer
. It's used when the app uses server-side rendering and to mount the components.
4. legacyRoot
: if you're dealing with a legacy app that requires rendering like in React v. 17 (with ReactDOM.render
), then you enable this option by setting legacyRoot: true
.
5. queries
: these are the queries to bind, and they override the default setting from DOM Testing Library unless they're merged.
Here's an example where we have a function to traverse table contents:
import * as tableQueries from 'my-table-query-library'
import {queries} from '@testing-library/react'
const {getByRowColumn, getByText} = render(<MyTable />, {
queries: {...queries, ...tableQueries},
})
4. renderHook Options
renderHook
is a wrapper around render
with a custom test component. Here's an example:
import {renderHook} from '@testing-library/react'
test('returns logged in user', () => {
const {result} = renderHook(() => useLoggedInUser())
expect(result.current).toEqual({name: 'Alice'})
})
But it comes with two essential options, one of which is the same wrapper
option that is also available for render
, where it's used to pass a React component to have it rendered around the inner element.
And the other option is the initialProps
option which declares the props that are passed to the render-callback when it's first invoked. One thing to note here is that these props will not be passed if you call rerender
without props:
import {renderHook} from '@testing-library/react'
test('returns logged in user', () => {
const {result, rerender} = renderHook(({name} = {}) => name, {
initialProps: {name: 'Alice'},
})
expect(result.current).toEqual({name: 'Alice'})
rerender()
expect(result.current).toEqual({name: undefined})
})
5. renderHook Result
The renderHook
method returns an object that has two properties:
1. result
: this holds the value of the most recently committed returned value of the render callback as shown below:
import {renderHook} from '@testing-library/react'
const {result} = renderHook(() => {
const [name, setName] = useState('')
React.useEffect(() => {
setName('Alice')
}, [])
return name
})
expect(result.current).toBe('Alice')
The value is held in result.current
. We can think of the result
property as a ref for the most recently committed value.
2. rerender
: this one renders the previously rendered callback with the new props:
import {renderHook} from '@testing-library/react'
const {rerender} = renderHook(({name = 'Alice'} = {}) => name)
rerender({name: 'Bob'})
Advance Level - React Testing Library Cheat Sheet
1. Adding custom queries
Before we jump to know about how to make a custom query, we should know that in most cases, we won't need to have one. But if we really need one, we need to make sure our custom queries encourage us to test in a user-centric way without testing the implementation details.
With that being clear, we can define our own custom queries in the following ways:
1. The helper functions are used to implement the default queries exposed by the DOM Testing Library. Like with the previously discussed render
method of React Testing Library. Here, we can add queries
to the options configuration object.
In the example below, we can see how to override the default testId
queries to use a different data attribute:
const domTestingLib = require('@testing-library/dom')
const {queryHelpers} = domTestingLib
export const queryByTestId = queryHelpers.queryByAttribute.bind(
null,
'data-test-id',
)
export const queryAllByTestId = queryHelpers.queryAllByAttribute.bind(
null,
'data-test-id',
)
export function getAllByTestId(container, id, ...rest) {
const els = queryAllByTestId(container, id, ...rest)
if (!els.length) {
throw queryHelpers.getElementError(
`Unable to find an element by: [data-test-id="${id}"]`,
container,
)
}
return els
}
export function getByTestId(container, id, ...rest) {
// result >= 1
const result = getAllByTestId(container, id, ...rest)
if (result.length > 1) {
throw queryHelpers.getElementError(
`Found multiple elements with the [data-test-id="${id}"]`,
container,
)
}
return result[0]
}
// re-export with overrides
module.exports = {
...domTestingLib,
getByTestId,
getAllByTestId,
queryByTestId,
queryAllByTestId,
}
2. Another method to have a custom query is by using the buildQueries
helper. This allows you to create custom queries with all the standard queries in the testing library. Here's an example:
import {queryHelpers, buildQueries} from '@testing-library/react'
const queryAllByDataCy = (...args) =>
queryHelpers.queryAllByAttribute('data-cy', ...args)
const getMultipleError = (c, dataCyValue) =>
`Found multiple elements with the data-cy attribute of: ${dataCyValue}`
const getMissingError = (c, dataCyValue) =>
`Unable to find an element with the data-cy attribute of: ${dataCyValue}`
const [
queryByDataCy,
getAllByDataCy,
getByDataCy,
findAllByDataCy,
findByDataCy,
] = buildQueries(queryAllByDataCy, getMultipleError, getMissingError)
export {
queryByDataCy,
queryAllByDataCy,
getByDataCy,
getAllByDataCy,
findAllByDataCy,
findByDataCy,
}
In the example above, a new set of query variants was created for getting elements by data-cy
, a test ID convention.
After making the custom queries, we can then use them in any render call using the queries
option. To add them globally, we can do the same by defining a custom render method as shown below:
import {render, queries} from '@testing-library/react'
import * as customQueries from './custom-queries'
const customRender = (ui, options) =>
render(ui, {queries: {...queries, ...customQueries}, ...options})
// re-export everything
export * from '@testing-library/react'
// override render method
export {customRender as render}
Now it's ready to use like any other query as follows:
const {getByDataCy} = render(<Component />)
expect(getByDataCy('my-component')).toHaveTextContent('Hello')
2. Skipping auto cleanup
React Testing Library's cleanup
utility unmounts React trees that were mounted before with render
. For example, if we are using the AVA testing framework, then we use test.afterEach
Hook as:
import {cleanup, render} from '@testing-library/react'
import test from 'ava'
test.afterEach(cleanup)
test('renders into document', () => {
render(<div />)
// ...
})
Now, this cleanup
is called after each test automatically if the testing framework supports the afterFetch
global. But, we can choose to skip the auto cleanup by setting the RTL_SKIP_AUTO_CLEANUP
environment variable to `true` as shown below with <a href="https://github.com/kentcdodds/cross-env" target="_blank">cross-env:</a>
cross-env RTL_SKIP_AUTO_CLEANUP=true jest
A simpler way is to import @testing-library/react/dont-cleanup-after-each
before library/react
in your test code. Here's an example with Jest's -setupFiles
config:
{
setupFiles: ['@testing-library/react/dont-cleanup-after-each']
}
3. Migrating from Enzyme to React Testing Library
The recommended way to ensure a successful migration is to do it incrementally by running two test libraries side by side in the same app and then porting our Enzyme tests to React Testing Library one by one.
Let's take this step by step:
1. Installing React Testing Library
First, we install the testing library along with the jest-dom
helper:
npm install --save-dev @testing-library/react @testing-library/jest-dom
// OR
yarn add --dev @testing-library/react @testing-library/jest-dom
2. Importing React Testing Library to your test
If you are using Jest, then you need to import the following modules to your test file:
import React from 'react'
import {render, screen} from '@testing-library/react'
As for the test structure, it's the same as you would write with Enzyme:
test('test title', () => {
// Your tests come here...
})
3. Migrating the tests
Let's say we have a Welcome
component that shows a welcome message:
const Welcome = props => {
const [values, setValues] = useState({
firstName: props.firstName,
lastName: props.lastName,
})
const handleChange = event => {
setValues({...values, [event.target.name]: event.target.value})
}
return (
<div>
<h1>
Welcome, {values.firstName} {values.lastName}
</h1>
<form name="userName">
<label>
First Name
<input
value={values.firstName}
name="firstName"
onChange={handleChange}
/>
</label>
<label>
Last Name
<input
value={values.lastName}
name="lastName"
onChange={handleChange}
/>
</label>
</form>
</div>
)
}
export default Welcome
This component gets a name
from props
showing a welcome message in an h1
element. It also has a text input which users can change to a different name. Now, if we want to test "to render the component and check that the h1
value is correct," then we have the following Enzyme test:
test('has correct welcome text', () => {
const wrapper = shallow(<Welcome firstName="John" lastName="Doe" />)
expect(wrapper.find('h1').text()).toEqual('Welcome, John Doe')
})
And in React Testing Library as:
test('has correct welcome text', () => {
render(<Welcome firstName="John" lastName="Doe" />)
expect(screen.getByRole('heading')).toHaveTextContent('Welcome, John Doe')
})
Enzyme's shallow
renderer doesn't render sub-components, while React Testing Library's render
method is more similar to Enzyme's mount
method. We don't need to assign the render
result to a variable or a wrapper
in React Testing Library. We can use the screen
object to access the rendered output.
4. Querying within elements
To query elements, we can use the within
utility function (which is an alias to getQueriesForElement
). This takes a DOM element and binds it to the raw query functions, and this allows them to be used without specifying a container.
For example, to get the text "Hello" only within a section called "messages", we can do something like this:
import {render, within} from '@testing-library/react'
const {getByText} = render(<MyComponent />)
const messages = getByText('messages')
const helloMessage = within(messages).getByText('hello')
5. Integration testing with React Testing Library
It's common to write unit tests for React components. But since writing React apps is about composing components, unit tests don't ensure a bug-free app.
That's why we need some integration tests that can help preserve peace of mind while we make changes to our React app because they ensure that the composition of components results in a better UX.
Now, to write integration tests with React Testing Library, we will take an example where we implement a scenario of the user entering their GitHub username and then the app displaying the list of their associated public repos.
Let's see how to do this step by step after installing the required testing packages:
1. Creating an integration test suite file: here, we create a file named viewGitHubRepositoriesByUsername.spec.js inside the ./test folder of our app so that Jest automatically picks it up.
2. Importing dependencies in the test file: we need to import the following dependencies to work:
import {
render,
cleanup,
waitForElement
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import 'jest-dom/extend-expect';
// Used to mock GitHub API
import nock from 'nock';
// Our test data
import {
FAKE_USERNAME_WITH_REPOS,
FAKE_USERNAME_WITHOUT_REPOS,
FAKE_BAD_USERNAME,
REPOS_LIST
} from './fixtures/github';
import App from '../App';
3. Setting up the test suite: we first need to mock the GitHub API before we start writing the tests. Then, after each test, we must clean the test React DOM so that the tests start from a clean point.
Here, we use the describe
and beforeAll
utilities to do the same, and to use the nock
function, we are able to call the API:
describe('view GitHub repositories by username', () => {
beforeAll(() => {
nock('https://api.github.com')
.persist()
.get(`/users/${FAKE_USERNAME_WITH_REPOS}/repos`)
.query(true)
.reply(200, REPOS_LIST);
});
afterEach(cleanup);
The describe
block specifies the integration test use case and the flow variants. So in those flow variants, we need to test the following:
describe('when GitHub user has public repositories', () => {
it('user can view the list of public repositories for entered GitHub username', async () => {
//...
});
});
describe('when GitHub user has no public repositories', () => {
it('user is presented with a message that there are no public repositories for entered GitHub username', async () => {
//...
});
});
describe('when GitHub user does not exist', () => {
it('user is presented with an error message', async () => {
//...
});
});
});
The it
blocks use async callback as needed.
4. Writing the flow test
First, we need to check whether the app is rendered or not; for that, we can use the render method from the library/react
package: -
const { getByText, getByPlaceholderText, queryByText } = render(<app>);</app>
Now, for the first step of the flow test, the user is presented with a username field and types a username string:
userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_USERNAME_WITH_REPOS);
The userEvent
helper utility has a type method that imitates the user behavior when they type text in a text field. The getByPlaceholder
query method returned earlier from render
allows us to find the DOM element by placeholder text.
Up next, we have to test whether the user clicks on the submit button and expects a list of repos:
userEvent.click(getByText('userSelection.submitButtonText').closest('button'));
getByText('repositories.header');
The userEvent.click
method imitates the user clicking a DOM element, and the getByText
query finds a DOM element by its text. The closest
modifier ensures that we have indeed selected the right element.
Next, we want to make sure that the app finishes fetching the repo list and displays it to the user:
await waitForElement(() => REPOS_LIST.reduce((elementsToWaitFor, repository) => {
elementsToWaitFor.push(getByText(repository.name));
elementsToWaitFor.push(getByText(repository.description));
return elementsToWaitFor;
}, []));
The waitForElement
async method is used to wait for a DOM update that will render the provided assertion as the method parameter is true. Here, we assert that the app displays the name and description for every repo that the mocked GitHub API returns.
And finally, we want to test that our app no longer displays a fetching indicator and instead should display an error message:
expect(queryByText('repositories.loadingText')).toBeNull();
expect(queryByText('repositories.error')).toBeNull();
The toBeNull
modifier checks for the non-existence of an element.
Conclusion
In this React Testing Library cheat sheet, we covered why this library is preferred over others, what problems it solves, coding a basic component render test, and in the end, accomplishing a complete integration test step by step.
We hope this React Testing Library cheat sheet will help you use this awesome library in your next React testing sprint.