Declarative and Scalable Testing With React Testing Library

Zerry Hogan
Dec 30, 2020 · 8 min read
A laptop with computer code displayed on the screen.
A laptop with computer code displayed on the screen.

“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: /components/ComplexForm/ComplexForm.ts

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.

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:

Image for post
Image for post

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 true or 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:

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:

JavaScript In Plain English

New JavaScript + Web Development articles every day.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store