You want to write maintainable tests for your React components. However, the de facto standard for testing (enzyme) is bloated with complexity and features, most of which encourage poor testing practices (mostly relating to testing implementation details).
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.
- Installation
- Usage
- More on
data-testid
s - Examples
- FAQ
- Other Solutions
- Guiding Principles
- Contributors
- 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
.
// __tests__/fetch.js
import React from 'react'
import {render, Simulate, flushPromises} from 'react-testing-library'
import axiosMock from 'axios'
import Fetch from '../fetch'
test('Fetch makes an API call and displays the greeting when load-greeting is clicked', async () => {
// Arrange
axiosMock.get.mockImplementationOnce(() =>
Promise.resolve({
data: {greeting: 'hello there'},
}),
)
const url = '/greeting'
const {queryByTestId, container} = render(<Fetch url={url} />)
// Act
Simulate.click(queryByTestId('load-greeting'))
// let's wait for our mocked `get` request promise to resolve
await flushPromises()
// Assert
expect(axiosMock.get).toHaveBeenCalledTimes(1)
expect(axiosMock.get).toHaveBeenCalledWith(url)
expect(queryByTestId('greeting-text').textContent).toBe('hello there')
expect(container.firstChild).toMatchSnapshot()
})
This is simply a re-export from the Simulate
utility from
react-dom/test-utils
. See the docs.
This is a simple utility that's useful for when your component is doing some
async work that you've mocked out, but you still need to wait until the next
tick of the event loop before you can continue your assertions. It simply
returns a promise that resolves in a setImmediate
. Especially useful when
you make your test function an async
function and use
await flushPromises()
.
See an example in the section about render
below.
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
.
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
const {container, unmount} = render(<Login />)
unmount()
// your component has been unmounted and now: container.innerHTML === ''
A shortcut to container.querySelector(`[data-testid="${yourId}"]`)
. Read
more about data-testid
s below.
const usernameInputElement = queryByTestId('username-input')
The queryByTestId
utility is referring to the practice of using data-testid
attributes to identify individual elements in your rendered component. This is
one of the practices this library is intended to encourage.
Learn more about this practice in the blog post: "Making your UI tests resilient to change"
You'll find examples of testing with different libraries in the test directory. Some included are:
Feel free to contribute more!
How do I update the props of a rendered component?
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, the easiest way to do that is:
const {container, queryByTestId} = render(<NumberDisplay number={1} />)
expect(queryByTestId('number-display').textContent).toBe('1')
// re-render the same component with different props
// but pass the same container in the options argument.
// which will cause a re-render of the same instance (normal React behavior).
render(<NumberDisplay number={2} />, {container})
expect(queryByTestId('number-display').textContent).toBe('2')
Open the tests for a full example of this.
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 {queryByTestId} = render(<HiddenMessage initialShow={true} />)
expect(queryByTestId('hidden-message')).toBeTruthy() // we just care it exists
// hide the message
Simulate.click(queryByTestId('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')).toBeFalsy() // 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?"
I don't want to use data-testid
attributes for everything. Do I have to?
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 queryByTestId
:
const items = [
/* your items */
]
const {queryByTestId} = render(/* your component with the items */)
const thirdItem = queryByTestId(`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 less your tests resemble the way your software is used, the less 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.
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 less your tests resemble the way your software is used, the less 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):
Kent C. Dodds 💻 📖 🚇 |
Ryan Castner 📖 |
Daniel Sandiego 💻 |
Paweł Mikołajczyk 💻 |
Alejandro Ñáñez Ortiz 📖 |
---|
This project follows the all-contributors specification. Contributions of any kind welcome!
MIT