This is the first in a series of articles that will help you build a solid foundation for testing React components.
At the beginning of the year, I set a goal to improve my code quality by focusing more on testing. I tried a handful of libraries and methods like Enzyme, react-test-render, and snapshot testing. I eventually landed on react-testing-library.
react-testing-library (RTL) is a testing framework created by Kent C. Dodds with the guiding principle of
The more your tests resemble the way your software is used, the more confidence they can give you.
RTL structures your test in a way that doesn’t include the implementation details of your components. As such, when your components are refactored in the future (to change implementation but not functionality), your tests do not break and your time isn’t spent fixing it.
Unlike Enzyme, RTL will test DOM nodes instead of component instances if said component is related to rendering. If you’d like an in-depth analysis, Kent C. Dodds has a comprehensive article about his dislikes with shallow rendering in Enzyme.
The TL;DR is that RTL does not include a lot of the utilities that Enzyme has (such as shallow
and instance
) because they
are things which users of your component cannot do, so your tests shouldn’t do them either.
We’ll create some basic tests for asserting form inputs. Here’s what our component will look like:
MyInput.js (but it’s actually a form 🤫)
inputIsPristine = true
and number = 0
with the useState
hookhandleChange
function and set inputIsPristine = false
and number
to the user’s inputerror-msg
is shown with inputHasError
when the input is no longer pristine (user types something) nor valid (less than 1)submit-btn
is disabled when the input is not valid (less than 1)data-testid
is an attribute used by RTL to find elementsNote: I call the component MyInput and I realize it isn’t technically an input since it’s wrapped in a form, but I did it so I could have an easy way to use the submit button with the input.
Assuming you already have jest installed in your app, we will also be installing a few other libraries. Here’s what the top of your test file will look like:
MyInput.test.js
jest-dom/extend-expect
react-testing-library
mounts our components to document.body
, we will be using its lceanup-after-each
which will unmount and clean up our DOM after each est
react-testing-library
's render
and fireEvent
functions later onThe render
method is the bread and butter of RTL. In this following example, I’m showing what RTL’s render
method is doing under the hood. My next example (getByLabelText) will demonstrate how to use render
in our tests.
What RTL’s render method sort of looks like
(ui)
that’s passed to it gets rendered on a container
getByLabelText
, etc) are all extracted and assigned to queries
The getByLabelText
utility works well for getting an input
element and its associated label
.
react-testing-library
's render
function mounts our component to the DOM and exposes a handful of utility methods (getByLabelText
in this case)'Number:’
to getByLabelText
and it returns the input
that was associated with the label
input
that it has an attribute type='number'
Note: getByLabelText
works here because of the relationship we created between the label
and input
in MyInput.js
. The htmlFor
in the label
element matches the id
in the input
element.
If we didn’t create the association between label
and input
or made a typo in the htmlFor
like calling it inpoot-number
, RTL would throw us an error:
RTL’s enforcement of this relationship is great for catching typos and keeping accessibility high!
This is a method I really like about RTL. Remember that data-testid
attribute we added to button
? The getByTestId
utility is what we’ll use to target it.
getByTestId
method to find an element with a data-testid="submit-btn"
defaultProps
where we set -->min = 1-->
, and our initial state where we set number = 0
— we expect our button to be disabled since our input is not validTypically, you’ll want to use getByTestId
when targeting a specific element — it’s great for looking up inconspicuous divs
or other elements in weird places.
This is a pretty general method that looks for any matching text that’s passed as an argument.
This is the same test we did with getByTestId
except here we use getByText
and pass it the button text “Submit!”
I tend to prefer using getByTestId
& data-testid
attributes over getByText
, as the former method reduces the scope and allows me to pinpoint the exact element I’m asserting.
The fireEvent
method is used to manipulate the DOM based on some sort of event. The list of events spans from change
to wheel
(the full list can be viewed here).
input
and button
variables to the elements that we found using the methods we used earlierfireEvent.change
and pass it an object that is similar to the one we used in our handleChange
function in MyInput.js
handleChange in MyInput.js
handleChange
function we set the number state with event.target.value
, so in the same fashion with our test we pass it the object { target: { value: 5 } }
min
(which is set as 1 from our defaultProps
) should be a valid input, thus enabling the buttonThis is a two-parter to show how you can test for something to NOT be in the DOM.
For rerender
, I typically use it to assert the outcome of a component after updating its props.
queryByTestId
is very similar to getByTestId
. The difference between the two is that getByTestId
will throw an error when it can’t find the element and queryByTestId
will return null
. All the other getBy
.. methods have similar queryBy
.. methods.
input
to the element associated with the label text “Number:”fireEvent.change
and change the input to -5
min (1)
which is defined in our defaultProps
, so we expect the error-msg
to be presentrerender
on our MyInput
component and update the min
prop to -10
-5
, is greater than our updated min
-10
, we expect the error-msg
to not exist (toBeNull
)RTL’s debug
method will log out the current structure of the DOM where ever you place it. Very useful for peeking at the state of your DOM during different parts of your tests.
debug
out of the render
methodfireEvent.change
Two debugs, two outputs!
fireEvent.change
we see our input’s value = "0"
fireEvent.change
we see that our input’s value = "-5"
and there’s also the inclusion of an error-msg div
nowHopefully, this walkthrough gave you a good idea on how you can implement RTL in your next (or even current) project. It’s a great way to test your components in a way that mimics your users’ interactions.
All of the examples can be found on github, or you can try it out instantly on repl.it.
If you have any other testing tips, don’t be afraid to send them my way! I just started my testing journey and would love to learn more about how to make my application more bulletproof. Also, be sure to stay tuned for the next part of this series where I will dive even deeper into testing! 🐐