Testing code can be intimidating if you don’t know how it works. We will go through the basics
Meet James. James is part of a large development team. He specializes in creating awesome React components, and together with many colleagues, he is building a large frontend.
Right now, he is building a brand new button.
This button has one important functional requirement: After a user clicks on it, it needs to become disabled. It’s no problem creating this, and James pushes his code into the repository so that it becomes available for all other developers when they pull in the latest changes.
But wait. That’s quite scary, isn’t it? What if another developer from the team changes the code? What if the button no longer becomes disabled after a user clicks on it?
This is where testing comes into play.
James can add a test for his new button component that asserts that the button becomes disabled after clicking on it (we will describe how a test actually does this a bit further down below). And whenever someone changes the code for the button component, and (accidentally or not) changes it in a way so that the button no longer is disabled after a user clicks on it, the test will fail.
With big red alerts. Systems and pipelines will stop, break, scream and cry. Emails are sent automatically, stating that the button no longer becomes disabled after a user clicks on it!
This is a bit of a childish example of course. But hopefully, it explains the concept of testing. As a matter of fact, this is exactly how large projects ensure that nothing “breaks.”
See for yourself: Examples of tests can be found over at React, lodash, date-fns, and pretty much every other project that respects itself. This is how many developers can work together on one project and make sure they don’t break each other’s code and functionality.
Let’s dive a bit more into testing!
We’ll explain all the details along the way. The abstract example above was explained in a simple matter, but a few questions are to be answered.
Very basic test
Before we look at how we can run tests, let’s have a look at a very minimal code example:
The button component is a very simple React component. It adds an HTML button element to the DOM when it is rendered.
But let’s have a look at the test itself. First, we import a
renderfunction from the React testing-library and the button component (line 7-8).
Then we call a function called
it (which is an alias for a function called “test,” which is a global function from Jest). We describe what we are testing (the
name for this little test), and we pass in an arrow function containing the test’s logic.
The logic can be found at lines 11–13. On line 11, we render our
Button component. Notice that the render function returns — amongst other things — a container. The container is an HTML element containing the elements that are returned by the render function. It’s the root of the document in which we execute our tests (see the note about this container below).
After the button is rendered inside the container (line 11), we query the container for a button element (line 12). And finally, on line 13, we make our first assertion:
Reading this line speaks for itself due to the readable syntax of Jest. In our test, we expect that the value for the variable
button is not null. And when the test runs (more about how to run tests soon), it succeeds, and everyone is happy!
Note: Normally, you don’t have to work with the render function’s container reference. We will look at several helper functions soon that are more convenient to work with. The default ESLint configuration in CRA (Create React App) is even complaining when your work with native node-related functions such as querySelector. But we wanted to illustrate a very minimal example that most developers will understand at first glance.
But now, what if a junior developer changed the code for our button component to the following? Let’s say they replaced the HTML button element with a div element inside the button component (line 2):
When the test runs again, now it will fail. Because it does not find an HTML button element, the value for the
button variable will be null:
Hopefully, the junior developer now starts a dialogue with the author of the failing test, and together, they will find out what to do. They either have to change the test or agree they will not change the button element into a div element.
If this test did not exist, nobody would notice the
button element was changed into a div. And again, this is an oversimplified example. But hopefully, you slowly start to understand how good tests for our components and code can make our lives better, especially when we’re collaborating with other people.
Note: tests are not only useful when you work together with other people. Even when you have a private project just for yourself, writing good tests (at least for critical parts of your code) can prevent you from getting some headaches in the future, when you have forgotten why you wrote certain pieces of logic in the way you did. Tests are guarding your code, we could say. Without them, you can easily introduce bugs without being aware of it!
Before we look at how we can run tests, it’s worth mentioning that we will often work with elements in the DOM when we write our tests. The testing-library project contains a great package called jest-dom, which provides a set of helper functions for making assertions related to the DOM.
You can view all matchers (as they are called) in the jest-dom readme file.
Jest-dom is included by default when you create a new application with CRA (Create React App).
Running tests with CRA (Create React App)
Create a new application with CRA:
$ npx create-react-app MyTestApplication
When the installation is done, go into the directory and run the tests for this project:
$ cd MyTestApplication
$ yarn run test --watchAll
Note: how do we use the
watchAllflag to ensure that all tests are executed. If we don’t provide this flag, only tests that you make changes to will run.
There is one test (
src/App.test.js), and it succeeded! Let’s look at the content of that file:
After everything we discussed above, this test should make sense by now. Except for one thing.
We don’t work with the container reference anymore. Instead, we can utilize the screen reference that’s imported on line 1. This screen instance has several helper functions (such as
getByText ) that we can use to find elements on our virtual “screen” (eg, the DOM). In this case, we query for any HTML element that contains the text “learn react.”
And finally, we expect that element to be in the document (DOM).
This is how we run tests manually in any CRA project. It’s a nice way of getting your hands dirty for the first time because you don’t have to install and configure Jest.
You can read more about installing and configuring Jest over here if you’re interested. It’s not that complicated.
One last thing is important to mention. The way Jest is configured for CRA projects is that it will execute tests for:
- Files with
- Files with
You can read more about the setup over here. And it can be customized, of course.
Adding tests to your repositories can protect your code, so to speak. It can assert and check that your code is doing what you expect it to do. And if it doesn’t, the tests will fail.
In this article, we executed tests manually. But they can (and should) be added to your pipelines if you have a continuous integration setup in order to prevent code from being if any of the tests fail.
I remember being extremely sceptical about testing when I first learned about it, many moons ago. But slowly but surely I figured out how powerful it was. And how nice it is to have them “watching over your code.”
We only scratched the surface in this article, so perhaps I will dive into some more advanced topics soon.
Thanks for your time!