You want to write maintainable tests for your React components. As a part of this goal, you want your tests to avoid including implementation details of your components and rather focus on making your tests give you the confidence for which they are intended. As part of this, you want your testbase to be maintainable in the long run so refactors of your components (changes to implementation but not functionality) don't break your tests and slow you and your team down.
The react-testing-library
is a very light-weight solution for testing React
components. It provides light utility functions on top of react-dom
and
react-dom/test-utils
, in a way that encourages better testing practices.
It's primary guiding principle is:
The more your tests resemble the way your software is used, the more confidence they can give you.
So rather than dealing with instances of rendered react components, your tests
will work with actual DOM nodes. The utilities this library provides facilitate
querying the DOM in the same way the user would. Finding for elements by their
label text (just like a user would), finding links and buttons from their text
(like a user would). It also exposes a recommended way to find elements by a
data-testid
as an "escape hatch" for elements where the text content and label
do not make sense or is not practical.
This library encourages your applications to be more accessible and allows you to get your tests closer to using your components the way a user will, which allows your tests to give you more confidence that your application will work when a real user uses it.
This library is a replacement for enzyme. While you can follow these guidelines using enzyme itself, enforcing this is harder because of all the extra utilities that enzyme provides (utilities which facilitate testing implementation details). Read more about this in the FAQ below.
What this library is not:
- A test runner or framework
- Specific to a testing framework (though we recommend Jest as our preference, the library works with any framework)
NOTE: This library is built on top of
dom-testing-library
which is where most of the logic behind the queries is.
- Installation
- Usage
TextMatch
query
APIsqueryAll
andgetAll
APIs- Examples
- Learning Material
- FAQ
- Other Solutions
- Guiding Principles
- Contributors
- Issues
- LICENSE
This module is distributed via npm which is bundled with node and
should be installed as one of your project's devDependencies
:
npm install --save-dev react-testing-library
This library has a peerDependencies
listing for react-dom
.
You may also be interested in installing jest-dom
so you can use
the custom jest matchers
// __tests__/fetch.js
import React from 'react'
import {render, Simulate, wait} from 'react-testing-library'
// this add custom jest matchers from jest-dom
import 'jest-dom/extend-expect'
import axiosMock from 'axios' // the mock lives in a __mocks__ directory
import Fetch from '../fetch' // see the tests for a full implementation
test('Fetch makes an API call and displays the greeting when load-greeting is clicked', async () => {
// Arrange
axiosMock.get.mockResolvedValueOnce({data: {greeting: 'hello there'}})
const url = '/greeting'
const {getByText, getByTestId, container} = render(<Fetch url={url} />)
// Act
Simulate.click(getByText('Load Greeting'))
// let's wait for our mocked `get` request promise to resolve
// wait will wait until the callback doesn't throw an error
await wait(() => getByTestId('greeting-text'))
// Assert
expect(axiosMock.get).toHaveBeenCalledTimes(1)
expect(axiosMock.get).toHaveBeenCalledWith(url)
expect(getByTestId('greeting-text')).toHaveTextContent('hello there')
expect(getByTestId('ok-button')).toHaveAttribute('disabled')
// snapshots work great with regular DOM nodes!
expect(container.firstChild).toMatchSnapshot()
})
In the example above, the render
method returns an object that has a few
properties:
The containing DOM node of your rendered React Element (rendered using
ReactDOM.render
). It's a div
. This is a regular DOM node, so you can call
container.querySelector
etc. to inspect the children.
Tip: To get the root element of your rendered element, use
container.firstChild
.NOTE: When that root element is a React Fragment,
container.firstChild
will only get the first child of that Fragment, not the Fragment itself.
This method is a shortcut for console.log(prettyDOM(container))
.
import {render} from 'react-testing-library'
const HelloWorld = () => <h1>Hello World</h1>
const {debug} = render(<HelloWorld />)
debug()
// <div>
// <h1>Hello World</h1>
// </div>
Learn more about prettyDOM
below.
It'd probably be better if you test the component that's doing the prop updating to ensure that the props are being updated correctly (see the Guiding Principles section). That said, if you'd prefer to update the props of a rendered component in your test, this function can be used to update props of the rendered component.
import {render} from 'react-testing-library'
const {rerender} = render(<NumberDisplay number={1} />)
// re-render the same component with different props
rerender(<NumberDisplay number={2} />)
Open the tests for a full example of this.
This will cause the rendered component to be unmounted. This is useful for testing what happens when your component is removed from the page (like testing that you don't leave event handlers hanging around causing memory leaks).
This method is a pretty small abstraction over
ReactDOM.unmountComponentAtNode
import {render} from 'react-testing-library'
const {container, unmount} = render(<Login />)
unmount()
// your component has been unmounted and now: container.innerHTML === ''
This will search for the label that matches the given TextMatch
,
then find the element associated with that label.
import {render} from 'react-testing-library'
const {getByLabelText} = render(<Login />)
const inputNode = getByLabelText('Username')
// this would find the input node for the following DOM structures:
// The "for" attribute (NOTE: in JSX with React you'll write "htmlFor" rather than "for")
// <label for="username-input">Username</label>
// <input id="username-input" />
//
// The aria-labelledby attribute
// <label id="username-label">Username</label>
// <input aria-labelledby="username-label" />
//
// Wrapper labels
// <label>Username <input /></label>
//
// It will NOT find the input node for this:
// <label><span>Username</span> <input /></label>
//
// For this case, you can provide a `selector` in the options:
const inputNode = getByLabelText('username', {selector: 'input'})
// and that would work
// Note that <input aria-label="username" /> will also work, but take
// care because this is not a label that users can see on the page. So
// the purpose of your input should be obvious for those users.
Note: This method will throw an error if it cannot find the node. If you don't want this behavior (for example you wish to assert that it doesn't exist), then use
queryByLabelText
instead.
This will search for all elements with a placeholder attribute and find one
that matches the given TextMatch
.
import {render} from 'react-testing-library'
const {getByPlaceholderText} = render(<input placeholder="Username" />)
const inputNode = getByPlaceholderText('Username')
NOTE: a placeholder is not a good substitute for a label so you should generally use
getByLabelText
instead.
This will search for all elements that have a text node with textContent
matching the given TextMatch
.
import {render} from 'react-testing-library'
const {getByText} = render(<a href="/about">About βΉοΈ</a>)
const aboutAnchorNode = getByText('about')
This will return the element (normally an <img>
) that has the given alt
text. Note that it only supports elements which accept an alt
attribute:
<img>
,
<input>
,
and <area>
(intentionally excluding <applet>
as it's deprecated).
import {render} from 'react-testing-library'
const {getByAltText} = render(
<img alt="Incredibles 2 Poster" src="/incredibles-2.png" />,
)
const incrediblesPosterImg = getByAltText(/incredibles.*poster$/i)
A shortcut to container.querySelector(`[data-testid="${yourId}"]`)
(and it
also accepts a TextMatch
).
import {render} from 'react-testing-library'
const {getByTestId} = render(<input data-testid="username-input" />)
const usernameInputElement = getByTestId('username-input')
In the spirit of the guiding principles, it is recommended to use this only after
getByLabel
,getByPlaceholderText
orgetByText
don't work for your use case. Using data-testid attributes do not resemble how your software is used and should be avoided if possible. That said, they are way better than querying based on DOM structure. Learn more aboutdata-testid
s from the blog post "Making your UI tests resilient to change"
Render into document.body
. Should be used with cleanup.
renderIntoDocument
will return the same object as render
import {renderIntoDocument} from 'react-testing-library'
renderIntoDocument(<div />)
Unmounts React trees that were mounted with renderIntoDocument.
import {cleanup, renderIntoDocument} from 'react-testing-library'
afterEach(cleanup)
test('renders into document', () => {
renderIntoDocument(<div />)
// ...
})
Failing to call cleanup
when you've called renderIntoDocument
could
result in a memory leak and tests which are not idempotent
(which can
lead to difficult to debug errors in your tests).
This is simply a re-export from the Simulate
utility from
react-dom/test-utils
. See the docs.
Note: Simulate
does not simulate browser events, meaning if you have an element like
<button type="submit">Submit</button>
calling Simulate.click
will not cause the submit event to be invoked. In order to get around this and for more info, see fireEvent
.
In general, it is better to use fireEvent
whenever possible because it mimics more closely what happens in the browser when an event happens.
Defined as:
function wait(
callback?: () => void,
options?: {
timeout?: number
interval?: number
},
): Promise<void>
When in need to wait for non-deterministic periods of time you can use wait
,
to wait for your expectations to pass. The wait
function is a small wrapper
around the
wait-for-expect
module.
Here's a simple example:
import {render, wait} from 'react-testing-library'
test('waiting for an expectation to pass before proceeding', async () => {
const {getByLabelText} = render(<Login />)
// wait until the callback does not throw an error. In this case, that means
// it'll wait until we can get a form control with a label that matches "username"
await wait(() => getByLabelText('username'))
getByLabelText('username').value = 'chucknorris'
})
This can be useful if you have a unit test that mocks API calls and you need to wait for your mock promises to all resolve. This can also be useful when (for example) you integration test your apollo-connected react components that go a couple level deep, with queries fired up in consequent components.
The default callback
is a no-op function (used like await wait()
). This can
be helpful if you only need to wait for one tick of the event loop.
The default timeout
is 4500ms
which will keep you under
Jest's default timeout of 5000ms
.
The default interval
is 50ms
. However it will run your callback immediately
on the next tick of the event loop (in a setTimeout
) before starting the
intervals.
NOTE:
wait
's callback can be called many times and because of this you should use as fewwait
calls as possible in each test and put minimum amount of code that absolutely needs to be waited on inside eachwait
'scallback
. This will help your tests run faster and can avoid unnecessary runtime complexity.
See dom-testing-library#waitForElement
import {render, waitForElement} from 'react-testing-library'
test('waiting for an element', async () => {
const {getByText} = render(<SearchForm />)
await waitForElement(() => getByText('Search'))
})
Example
import {render, Simulate, waitForElement} from 'react-testing-library'
test('should submit form when valid', async () => {
const mockSubmit = jest.fn()
const {
container,
getByLabelText,
getByText
} = render(<Form onSubmit={mockSubmit} />)
const nameInput = getByLabelText('Name')
nameInput.value = 'Chewbacca'
Simulate.change(nameInput)
+ // wait for button to appear and click it
+ const submitButton = await waitForElement(() => getByText('Search'))
+ Simulate.click(submitButton)
+ expect(mockSubmit).toBeCalled()
})
Fire DOM events.
React attaches an event handler on the document
and handles some DOM events
via event delegation (events bubbling up from a target
to an ancestor). Because
of this, your node
must be in the document.body
for fireEvent
to work with
React. You can render into the document using the
renderIntoDocument utility. This is an alternative to
simulating Synthetic React Events via Simulate. The benefit of
using fireEvent
over Simulate
is that you are testing real DOM events
instead of Synthetic Events. This aligns better with
the Guiding Principles.
NOTE: If you don't like having to render into the document to get
fireEvent
working, then feel free to try to chip into making it possible for React to attach event handlers to the rendered node rather than thedocument
. Learn more here: facebook/react#2043
import {renderIntoDocument, cleanup, fireEvent} from 'react-testing-library'
// don't forget to clean up the document.body
afterEach(cleanup)
test('clicks submit button', () => {
const spy = jest.fn()
const {getByText} = renderIntoDocument(<button onClick={spy}>Submit</button>)
fireEvent(
getByText('Submit'),
new MouseEvent('click', {
bubbles: true, // click events must bubble for React to see it
cancelable: true,
}),
)
expect(spy).toHaveBeenCalledTimes(1)
})
Convenience methods for firing DOM events. Check out
dom-testing-library/src/events.js
for a full list as well as default eventProperties
.
import {renderIntoDocument, fireEvent} from 'react-testing-library'
const {getElementByText} = renderIntoDocument(<Form />)
// similar to the above example
// click will bubble for React to see it
const rightClick = {button: 2}
fireEvent.click(getElementByText('Submit'), rightClick)
// default `button` property for click events is set to `0` which is a left click.
This helper function can be used to print out readable representation of the DOM tree of a node. This can be helpful for instance when debugging tests.
It is defined as:
function prettyDOM(node: HTMLElement, maxLength?: number): string
It receives the root node to print out, and an optional extra argument to limit the size of the resulting string, for cases when it becomes too large.
This function is usually used alongside console.log
to temporarily print out
DOM trees during tests for debugging purposes:
import {render, prettyDOM} from 'react-testing-library'
const HelloWorld = () => <h1>Hello World</h1>
const {container} = render(<HelloWorld />)
console.log(prettyDOM(container))
// <div>
// <h1>Hello World</h1>
// </div>
Several APIs accept a TextMatch
which can be a string
, regex
or a
function
which returns true
for a match and false
for a mismatch.
See dom-testing-library#textmatch for options.
Examples:
import {render, getByText} from 'react-testing-library'
const {container} = render(<div>Hello World</div>)
// WILL find the div:
// Matching a string:
getByText(container, 'Hello World') // full string match
getByText(container, 'llo Worl', {exact: false}) // substring match
getByText(container, 'hello world', {exact: false}) // ignore case
// Matching a regex:
getByText(container, /World/) // substring match
getByText(container, /world/i) // substring match, ignore case
getByText(container, /^hello world$/i) // full string match, ignore case
getByText(container, /Hello W?oRlD/i) // advanced regex
// Matching with a custom function:
getByText(container, (content, element) => content.startsWith('Hello'))
// WILL NOT find the div:
getByText(container, 'Goodbye World') // full string does not match
getByText(container, /hello world/) // case-sensitive regex with different case
// function looking for a span when it's actually a div:
getByText(container, (content, element) => {
return element.tagName.toLowerCase() === 'span' && content.startsWith('Hello')
})
Each of the get
APIs listed in the render
section above have a
complimentary query
API. The get
APIs will throw errors if a proper node
cannot be found. This is normally the desired effect. However, if you want to
make an assertion that an element is not present in the DOM, then you can use
the query
API instead:
import {render} from 'react-testing-library'
const {queryByText} = render(<Form />)
const submitButton = queryByText('submit')
expect(submitButton).toBeNull() // it doesn't exist
Each of the query
APIs have a corresponsing queryAll
version that always returns an Array of matching nodes. getAll
is the same but throws when the array has a length of 0.
import {render} from 'react-testing-library'
const {queryByText} = render(<Forms />)
const submitButtons = queryAllByText('submit')
expect(submitButtons).toHaveLength(3) // expect 3 elements
expect(submitButtons[0]).toBeInTheDOM()
You'll find examples of testing with different libraries in
the examples
directory.
Some included are:
-
A clear way to unit testing React JS components using Jest and react-testing-library by Richard Kotze
Feel free to contribute more!
Which get method should I use?
Based on the Guiding Principles, your test should resemble how your code (component, page, etc.) as much as possible. With this in mind, we recommend this order of priority:
getByLabelText
: Only really good for form fields, but this is the number 1 method a user finds those elements, so it should be your top preference.getByPlaceholderText
: A placeholder is not a substitute for a label. But if that's all you have, then it's better than alternatives.getByText
: Not useful for forms, but this is the number 1 method a user finds other elements (like buttons to click), so it should be your top preference for non-form elements.getByAltText
: If your element is one which supportsalt
text (img
,area
, andinput
), then you can use this to find that element.getByTestId
: The user cannot see (or hear) these, so this is only recommended for cases where you can't match by text or it doesn't make sense (the text is dynamic).
Other than that, you can also use the container
to query the rendered
component as well (using the regular
querySelector
API).
Can I write unit tests with this library?
Definitely yes! You can write unit and integration tests with this library. See below for more on how to mock dependencies (because this library intentionally does NOT support shallow rendering) if you want to unit test a high level component. The tests in this project show several examples of unit testing with this library.
As you write your tests, keep in mind:
The more your tests resemble the way your software is used, the more confidence they can give you. - 17 Feb 2018
What if my app is localized and I don't have access to the text in test?
This is fairly common. Our first bit of advice is to try to get the default
text used in your tests. That will make everything much easier (more than just
using this utility). If that's not possible, then you're probably best
to just stick with data-testid
s (which is not bad anyway).
If I can't use shallow rendering, how do I mock out components in tests?
In general, you should avoid mocking out components (see the Guiding Principles section). However if you need to, then it's pretty trivial using Jest's mocking feature. One case that I've found mocking to be especially useful is for animation libraries. I don't want my tests to wait for animations to end.
jest.mock('react-transition-group', () => {
const FakeTransition = jest.fn(({children}) => children)
const FakeCSSTransition = jest.fn(
props =>
props.in ? <FakeTransition>{props.children}</FakeTransition> : null,
)
return {CSSTransition: FakeCSSTransition, Transition: FakeTransition}
})
test('you can mock things with jest.mock', () => {
const {getByTestId, queryByTestId} = render(
<HiddenMessage initialShow={true} />,
)
expect(queryByTestId('hidden-message')).toBeTruthy() // we just care it exists
// hide the message
Simulate.click(getByTestId('toggle-message'))
// in the real world, the CSSTransition component would take some time
// before finishing the animation which would actually hide the message.
// So we've mocked it out for our tests to make it happen instantly
expect(queryByTestId('hidden-message')).toBeNull() // we just care it doesn't exist
})
Note that because they're Jest mock functions (jest.fn()
), you could also make
assertions on those as well if you wanted.
Open full test for the full example.
This looks like more work that shallow rendering (and it is), but it gives you more confidence so long as your mock resembles the thing you're mocking closly enough.
If you want to make things more like shallow rendering, then you could do something more like this.
Learn more about how Jest mocks work from my blog post: "But really, what is a JavaScript mock?"
What if I want to verify that an element does NOT exist?
You typically will get access to rendered elements using the getByTestId
utility. However, that function will throw an error if the element isn't found. If you want to specifically test for the absence of an element, then you should use the queryByTestId
utility which will return the element if found or null
if not.
expect(queryByTestId('thing-that-does-not-exist')).toBeNull()
I really don't like data-testids, but none of the other queries make sense. Do I have to use a data-testid?
Definitely not. That said, a common reason people don't like the data-testid
attribute is they're concerned about shipping that to production. I'd suggest
that you probably want some simple E2E tests that run in production on occasion
to make certain that things are working smoothly. In that case the data-testid
attributes will be very useful. Even if you don't run these in production, you
may want to run some E2E tests that run on the same code you're about to ship to
production. In that case, the data-testid
attributes will be valuable there as
well.
All that said, if you really don't want to ship data-testid
attributes, then you
can use
this simple babel plugin
to remove them.
If you don't want to use them at all, then you can simply use regular DOM methods and properties to query elements off your container.
const firstLiInDiv = container.querySelector('div li')
const allLisInDiv = container.querySelectorAll('div li')
const rootElement = container.firstChild
What if Iβm iterating over a list of items that I want to put the data-testid="item" attribute on. How do I distinguish them from each other?
You can make your selector just choose the one you want by including :nth-child in the selector.
const thirdLiInUl = container.querySelector('ul > li:nth-child(3)')
Or you could include the index or an ID in your attribute:
<li data-testid={`item-${item.id}`}>{item.text}</li>
And then you could use the getByTestId
utility:
const items = [
/* your items */
]
const {getByTestId} = render(/* your component with the items */)
const thirdItem = getByTestId(`item-${items[2].id}`)
What about enzyme is "bloated with complexity and features" and "encourage poor testing practices"?
Most of the damaging features have to do with encouraging testing implementation details. Primarily, these are shallow rendering, APIs which allow selecting rendered elements by component constructors, and APIs which allow you to get and interact with component instances (and their state/properties) (most of enzyme's wrapper APIs allow this).
The guiding principle for this library is:
The more your tests resemble the way your software is used, the more confidence they can give you. - 17 Feb 2018
Because users can't directly interact with your app's component instances, assert on their internal state or what components they render, or call their internal methods, doing those things in your tests reduce the confidence they're able to give you.
That's not to say that there's never a use case for doing those things, so they should be possible to accomplish, just not the default and natural way to test react components.
Why isn't snapshot diffing working?
If you use the snapshot-diff library to save snapshot diffs, it won't work out of the box because this library uses the DOM which is mutable. Changes don't return new objects so snapshot-diff will think it's the same object and avoid diffing it.
Luckily there's an easy way to make it work: clone the DOM when passing it into snapshot-diff. It looks like this:
const firstVersion = container.cloneNode(true)
// Do some changes
snapshotDiff(firstVersion, container.cloneNode(true))
In preparing this project, I tweeted about it and Sune Simonsen took up the challenge. We had different ideas of what to include in the library, so I decided to create this one instead.
The more your tests resemble the way your software is used, the more confidence they can give you.
We try to only expose methods and utilities that encourage you to write tests that closely resemble how your react components are used.
Utilities are included in this project based on the following guiding principles:
- If it relates to rendering components, it deals with DOM nodes rather than component instances, nor should it encourage dealing with component instances.
- It should be generally useful for testing individual React components or
full React applications. While this library is focused on
react-dom
, utilities could be included even if they don't directly relate toreact-dom
. - Utility implementations and APIs should be simple and flexible.
At the end of the day, what we want is for this library to be pretty light-weight, simple, and understandable.
Thanks goes to these people (emoji key):
This project follows the all-contributors specification. Contributions of any kind welcome!
Looking to contribute? Look for the Good First Issue label.
Please file an issue for bugs, missing documentation, or unexpected behavior.
Please file an issue to suggest new features. Vote on feature requests by adding a π. This helps maintainers prioritize what to work on.
For questions related to using the library, please visit a support community instead of filing an issue on GitHub.
MIT