Back

How to Write Functional Tests in React (Part 1)

Jeffrey Zhen
Jeffrey Zhen
April 12, 2019
How to Write Functional Tests in React (Part 1)

Photo by Annette Keys on Unsplash

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.

What is 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.

How is RTL different than other frameworks like Enzyme?

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.

How can I use RTL effectively?

We’ll create some basic tests for asserting form inputs. Here’s what our component will look like:

Our Component

MyInput.js (but it’s actually a form 🤫)

  • On the initial creation of this functional component, we assign inputIsPristine = true and number = 0 with the useState hook
  • When the user types in the input we fire the handleChange function and set inputIsPristine = false and number to the user’s input
  • The error-msg is shown with inputHasError when the input is no longer pristine (user types something) nor valid (less than 1)
  • The submit-btn is disabled when the input is not valid (less than 1)
  • data-testid is an attribute used by RTL to find elements

Note: 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.

Our Test File

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

  • We will be getting a few extra and helpful assertions from jest-dom/extend-expect
  • Because 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
  • I will go into detail about react-testing-library's render and fireEvent functions later on

Our Test Utilities

render

The 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

  • The component (ui) that’s passed to it gets rendered on a container
  • RTL’s utility methods (ie. getByLabelText, etc) are all extracted and assigned to queries
  • These methods are then spread on the object that is returned so that we can destructure out the utilities that we need

getByLabelText

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)
  • We pass the text of the label 'Number:’ to getByLabelText and it returns the input that was associated with the label
  • We then make an assertion on the 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!

getByTestId

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.

  • We use RTL’s getByTestId method to find an element with a data-testid="submit-btn"
  • Because of 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 valid

Typically, 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.

getByText

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.

fireEvent

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).

  • First, we assign input and button variables to the elements that we found using the methods we used earlier
  • Next, we call fireEvent.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

  • In our 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 } }
  • Finally, we expect that the button is enabled because any number greater than or equal to our min (which is set as 1 from our defaultProps) should be a valid input, thus enabling the button

rerender & queryByTestId

This 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.

  • Just like before, we assign the variable input to the element associated with the label text “Number:”
  • We call fireEvent.change and change the input to -5
  • The input’s value is now less than our min (1) which is defined in our defaultProps, so we expect the error-msg to be present
  • Now we call rerender on our MyInput component and update the min prop to -10
  • Now that our input’s value -5, is greater than our updated min -10, we expect the error-msg to not exist (toBeNull)

Bonus: debug

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.

  • Deconstruct debug out of the render method
  • Here we place it before and after we call fireEvent.change
  • Run our test and we’ll see this in our console:

Two debugs, two outputs!

  • In the first output, before we call fireEvent.change we see our input’s value = "0"
  • In the second output, after we call fireEvent.change we see that our input’s value = "-5" and there’s also the inclusion of an error-msg div now

Conclusion

Hopefully, 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! 🐐

Share this post

Interested in working with us?

Give us some details about your project, and our team will be in touch within a day or two.