“All code is guilty, until proven innocent.” — Anonymous
Let’s be honest, writing tests for your React components is probably not your favorite thing to do. It can often seem cumbersome, difficult and annoying. We often don’t really know what to test or even how to test our components. But, the reality is that testing is extremely important to the integrity of your application and when done correctly it can provide you with a feeling of confidence that your application works the way you intend.
There are plenty of articles that give examples of how to use React Testing Library on a small scale but not many discuss how to write clean tests and how to make testing easy for large projects with multiple developers. I’d like to share what I’ve learned from writing tests for large projects with complex components and lots of moving pieces. My goal is to show you how to test your React components in such a way that writing tests becomes a breeze and lack of code coverage becomes a thing of the past.
Let’s get it.
First, we’ll initialize a new React app. I will be using TypeScript in this article.
npx create-react-app test-app --template typescript# oryarn create react-app test-app --template typescript
We are also going to install another testing library to makes it easy to interact with our components the way a user would. The library is called
@testing-library/user-event and it will make it super easy to simulate real-world user events like clicking and typing in your test cases.
npm install @testing-library/user-event --save-dev# oryarn add @testing-library/user-event --dev
Now, open up the project in your IDE of choice and under
src create the following file named:
Paste the code below in the file:
Then, in your
App.tsx file which will be on the root of your
src folder you will paste the following code:
Before we move on I will explain what this component is doing.
- We are creating a component called
ComplexFormthat renders a simple HTML Form for a user to interact with.
- The form asks the user to enter their first name, last name and check whether or not they are over 21 years old.
- If the user is over 21 years old, then another input is rendered asking the user what their favorite drink is.
- After the form is filled out a user can either click Apply or Cancel. Both buttons will trigger callback props that are passed into the ComplexForm as you can see in App.tsx.
- If a user clicks submit, we convert the forms elements into JSON to pass into the onSubmit callback.
Now that you understand what all of that code is doing, start the project by using:
$ cd test-app
$ yarn start
You should see a form that looks something like this:
Now we have our sample component ready to test but first, let’s talk a little bit about React Testing Library.
Why React Testing Library?
React Testing Library is a library designed for testing React components. You may have used Enzyme in the past to test your React components. Where React Testing Library differs from Enzyme is that it renders your tests using actual DOM nodes rather than instances of React components.
What this means for your tests is that your test cases will run in an environment that is similar to the real environment that your users will be running your application in i.e. a web browser. The closer your testing environment resembles the environment your users will use your application in the more confidence you can have in your tests.
Another big reason I prefer using React Testing Library is because of the libraries philosophy which essentially states that your tests should resemble the way your users will interact with your app. When one of your users is using your app they are not aware of the fact that they are interacting with state and props. They don’t care if you’re using hooks in functional components or higher order components with class components. Your users see user interfaces (buttons, inputs, modals, etc…) and that’s what they interact with.
So, rather than testing whether or not the correct props or state were changed in your component, React Testing Library is designed in such a way that you have to test what your users are seeing and doing. This encourages you to build accessible user interfaces and adhere to best practices when structuring your HTML.
Applying the Philosophy
So, how does the philosophy of React Testing Library apply to our example component and how do we know what to test? Well, let’s think about the way the user will interact with this component.
Test Case 1
What is the first thing that a user will see when they load this app? Well, they should see a heading with a first and last name input, a checkbox asking them if they are over 21 and a cancel and submit button. I always like to write a default test case which tests what the user should see at first.
Test Case 2
The next logical interaction for a user is to start filling out the form. So, a user will start filling out the form and then they will get to the “Are you at least 21 years old?” checkbox. If a user clicks yes, then we show another input conditionally for them to enter their favorite drink. This represents a separate branch of code we need to test.
Notice how this test does not directly test our usage of
useState. We are testing that our user sees the correct information not that the internal state changed to
false. We could refactor the state logic to use
useReducer or any other state management solution and the test case would never have to change.
Test Cases 3 and 4
The final things that a user can do in this component are either click Cancel or Submit. The way that this component is designed is that a parent component will pass in callbacks for when the Cancel or Submit buttons are clicked. So, these test cases slightly differ from the previous ones. We won’t be testing what the user sees but rather we’ll want to test that our code internally reacts correctly to a users action by calling specific functions.
Writing Tests Using Declarative Programming
So, we have set up our test component, we’ve discussed what React Testing Library is and we applied the philosophy of React Testing Library to create our test cases. It’s now time to actually start writing tests!
You will often see developers write tests like this:
There is nothing inherently wrong with this test and if your component is really simple and only requires 1 or 2 tests then this would be perfectly fine. This type of testing becomes an issue when your components become complex and you start having 5, 10 or 15+ test cases for a single component.
If every test looked like this not only would your test file be large but it will be difficult for other devs and your future self to quickly understand what is happening in the tests because they will have to carefully read each line of code to understand what you are doing.
Instead, what if our tests were declarative? What if instead of writing tests that show what we are doing we instead write test that describe the intent of the user? That might sound confusing at first but I’ll give you an example of what I mean. The test case above could be rewritten as such using declarative programming:
Does that test feel easier to read and understand? You can immediately understand what is going on by just simply reading allowed the functions. We are checking if the First Name and Last Name input are in the document. Then we are clicking the “Are you at least 21 years old?” checkbox and then we are checking if the Favorite Drink input is in the document.
Not only is this test much more readable but the testing helpers exported from
renderComplexForm function can be re-used in other test cases. So, if we had 10 or 20 test cases total we would have to write and repeat far less code and achieve much greater readability. If another developer had to add features to this component 6 months later and needed to update the tests they would have a much easier time updating the tests.
I have found writing tests this way scales really well with large projects and also makes testing complex components easier.
Writing Tests for the ComplexForm Component
Finally, let’s apply this testing methodology to our
ComplexForm component and write the actual test spec for the 4 test cases we documented above. Here is the final code:
Let’s break this down a bit:
- First we create our render function for our component. The render function is responsible for rendering our component using React Testing Library and exporting helper functions for our test cases. You could also create a separate file for the render function and import it into your test.
- In each test case we call
renderComplexFormand grab the utility functions we need for that specific test case.
- We created test helper functions for changing input values such as
changeFirstName. This simulates how the user would interact and makes it very apparent what is happening in the test.
renderComplexFormfunction accepts a props argument. It’s often the case that your components will be able to accept props that change the ui of your component or what the user sees. By allowing each test case to pass in props we can test for different interactions in each test case as well.
- We are using jest mock functions for the
onCancelprops. Jest mock functions are great for testing if a function was called, how many times it was called and what arguments was it called with. Jest mock functions were used in the last 2 test cases to test that user button clicks triggered the appropriate callback functions.
The end result I believe are tests that are more readable, scalable and durable. I could come back to this test and add additional inputs and be able to update the tests in a few minutes rather than having to parse through the test code to know where to add my new test coverage.
Let me know what you all think in the comments!
You can find the GitHub repo at: https://github.com/jerrywithaz/how-to-test-react-app