React.js & Jest testing

A beginner’s guide to testing React components using Jest and GitHub Actions

In this lesson, we are going to take a quick look at the Jest testing library and integrate it with a sample React project to test some simple React components. Later, we will automate this process using GitHub Actions.

Uday Hiwarale
Dec 3, 2020 · 14 min read
(source: unsplash.com)

Introduction to Jest

You might have heard about Mocha and Chai. Mocha is a testing library for JavaScript while Chai is an assertion library. Jest is another testing library for JavaScript, but unlike Mocha, it comes with its own expect type assertion API as well as a high-level browser-like DOM API using JsDom. It also comes with mocking, stubbing, and spying abilities as well as coverage generation.

There is a lot to explore, so let’s get started. The Jest library can be used on Node.js or inside a browser. To install it for Node.js, use the standard npm install command as follows.

$ npm install --save-dev jest

You can also install it globally using the --global flag which should give you the jest command. However, here we are installing it locally. We would still need to execute the jest command to ask Jest to test something but we would need to add that command inside the scripts of the package.json.

{
"name": "react-unit-testing",
"scripts": {
"test": "jest"
}
}

When we run the npm test or npm run test command, the jest command is executed by the NPM from the local installation of the Jest. By default, Jest matches all the files inside __tests__ directory as well as *.test.js, *.test.jsx, *.spec.js, *.spec.jsx files in the project. However, you can use the jest "regexp" command where regexp is the regular expression that matches the files to test. Here are all the jest command-line options.

We can also configure Jest using JSON without having to bloat the jest command. We can use the package.json to configure Jest by including a "jest" key or having a separate JSON file such as jest.config.json. You can also use a JavaScript file such as jest.config.js which exports a JavaScript object or a function (including async) that exports this object. Whether JSON or a JavaScript object, here are possible configurations. Using the --config flag with the jest command, we need to provide the path of this file.

In our case, let’s use the package.json itself to configure Jest (although jest command would have sufficed). Our project has the sample and src directories with .test.js and .test.jsx files containing the JavaScript tests. So we would need to tell Jest to find files ending with .test.js or .test.jsx inside these directories.

{
"name": "react-unit-testing",
"scripts": {
"test": "jest"
},
"jest": {
"verbose": true,
"roots": [ "sample", "src" ],
"testRegex": "\\.test\\.jsx?$"

}
}

As you can see from the above package.json file, the roots field controls which directories to look up for the test files while testRegex field provides a regular expression to find these files in the given directories. When the jest the command is executed, it will look for the jest key inside package.json and use this configuration to execute the tests.

💡 We are using the sample directory to hold some plain JavaScript tests while the src directory will contain actual React components and their tests.

So how does a Jest test file look like? Well, a Jest test file is simply a JavaScript file but with some unusual function calls. Let’s create a JavaScript module math.js and a test file math.test.js for it.

In the above example, our math.js exports add and divide function for simple arithmetics. In the math.test.js, we have imported these functions and written some tests to check the return value of these functions.

The test function is provided by the Jest which defines a test (AKA test case). The first argument to this function is the description of the test while the second argument is a function that contains JavaScript code that tests the functionality of our module. If an exception (Error) is thrown inside this function, the test fails.

💡 You can also use the it() function since it is an alias of test().

Jest provides expect function to assert an output value against an expected value. The toBe(expected) method matches the value provided to the expect(value) call with the expected value. Similarly, the toBeNull() method matches the value with null. These methods are collectively called matchers and you can find the entire list of matchers here.

If the output value doesn’t match the expected value, these methods throw an error and the test fails. By default, Jest executes all the tests whether or not previous tests have failed. To bail out if one or n tests have failed previously, use the bail configuration option.

In the above example, as you can see from the results in the console, 2 out of 3 tests are passed while the divide(4,0) to be null test failed because the return value was expected to be null. Therefore, we would need to go back and modify the divide method to handle this expectation.

Like the test function, Jest provides other functions such as beforeAll and afterAll that gets executed before running the first test and after executing the last test respectively. Similarly, the beforeEach and afterEach functions execute before and after each test run.

The describe function is used to club multiple tests together. We can use before* and after* functions in this block which would only apply to the tests in this block. To know more about these functions, read this documentation. But for now, let’s see how describe block works.

Functions like test, describe, beforeAll, afterEach, etc. are available to a test file only during runtime are we are not importing these functions explicitly. Therefore your IDE might not provide you the right IntelliSense for them. To enable the IntelliSense, install the @types/jest package.

Sometimes, you have to test an asynchronous function whose result is not immediately available. In that case, we can use either the callback approach, promise approach or async/await approach to instruct Jest that a test assertion has been completed.

In the above example, the multiply function returns the result in a promise that resolves after 1 second. In the test function, we would need to wait until that promise is resolved. Once the promise is resolved, we can make an assertion on the result and mark the test as completed.

We can either use a callback function such as done in the above case that should be executed once the assertion has been made. We could also return a promise from the test function that should resolve after the assertion has been made. Else, the test function can accept an async function that comes very handy when dealing with multiple promises. To know more about asynchronous testing with Jest, read this documentation.

Jest is a big library and it provides much more than what we have witnessed here. We will explore more about Jest while writing test cases of our React application but if you want to use Jest outside of React, follow this documentation.

In our case, Jest is running on Node. At this moment (2020), Node does not support ES6 imports and other new ECMAScript features. Therfore, we first need to transpile the JavaScript code to a lower JavaScript version such as ES5 so that we can run the tests on Node.

Babel is the most popular JavaScript transpiler. To integrate it with Jest, we first need to install the babel-jest package along with other Babel package for the transpilation. You can use the following command.

$ npm install -D babel-jest @babel/preset-env @babel/plugin-transform-runtime @babel/plugin-transform-async-to-generator @babel/plugin-proposal-export-default-from @babel/plugin-proposal-export-namespace-from

Once these packages are installed, we are ready to set up Jest and Babel integration. First, we need to tell Jest that we would need to transform our test files and any JavaScript modules they import using the babel-jest package. For this, we use the transform confuguration option.

{
"name": "react-unit-testing",
"scripts": {
"test": "jest"
},
"jest": {
"verbose": true,
"roots": [ "sample", "src" ],
"testRegex": "\\.test\\.jsx?$",
"transform": {
"\\.jsx?$": "babel-jest"
}

}
}

The transformIgnorePatterns configuration option controls the files to be ignored in transformation (transpilation) such as modules (files) from node_modules directory. Both these configuration options have a default value so you can ignore this setting in the first place and it would work just fine for you.

Next, we need to configure the Babel for transpilation. This can be done through an independent configuration file such as .babelrc or babel.config.js. In our case, we are going to use babel.config.js and provide presets and plugins we have installed in the previous step.

module.exports = {
presets: [
'@babel/preset-env'
],
plugins: [
'@babel/plugin-transform-runtime',
'@babel/plugin-transform-async-to-generator',
'@babel/plugin-proposal-export-default-from',
'@babel/plugin-proposal-export-namespace-from'
]
};

Now we have all we need. Once we run npm run test command, Jest will first transpile the files matched by the transform option and then run tests. Let’s modify our math.js module as well as math.test.js test and use ES6 import statements.

(sample/math.js)

As you can see from the above example, now we exporting functions from the math.js module using the export statement and importing them inside the test file using the import statement. Though these statements are legally invalid for Node.js but with the help of Babel, everything is running smoothly.

Integrating Jest with React

There are many ways to set up a React project such as create-react-app or a React project boilerplate such as this I have created. But in this article, we are not going to focus on how to set up a proper React project. Instead, we are going to set up a minimal implementation of React using Webpack.

First of all, we would need to install some packages to begin with. These packages are essential for React and its seamless integration with Webpack.

$ npm i -S react react-dom
$ npm i -D webpack webpack-cli webpack-dev-server html-webpack-plugin babel-loader @babel/preset-react

In the previous section, we learned how to transpile JavaScript code using Babel and @babel/preset-env package. Since React components are not strictly JavaScript as they might include JSX syntax, we also need to transpile them to pure JavaScript. For that, we need the @babel/preset-react package and include it inside the presets of the babel.config.js.

module.exports = {
presets: [
'@babel/preset-env',
'@babel/preset-react'
],
plugins: [
'@babel/plugin-transform-runtime',
'@babel/plugin-transform-async-to-generator',
'@babel/plugin-proposal-export-default-from',
'@babel/plugin-proposal-export-namespace-from'
]
};

Once these packages are installed, we can move on to setting up the Webpack configuration. The webpack.config.js configuration file will help up compile the React project and start a local development server. We are not going to focus on integrating Jest with Webpack, for that follow this documentation.

(source: gist.github.com)

For simplicity, use the script key of the package.json to add some Webpack related command as shown below. The npm run start command will start a local development server and serve the React application in the browser on 9000 port. So you would be able to see the React application in the browser from the http://localhost:9000 URL.

{
"name": "react-unit-testing",
"scripts": {
"test": "jest",
"start": "NODE_ENV=development webpack-dev-server",
"build": "NODE_ENV=production webpack"

}
}

From the above code snippet, we are bootstrapping the App React component inside an HTML element identified with the id app. This component is located inside src/components/app directory. Let’s create a simple functional React component with a counter that increments when a button is pressed.

(src/components/app/app.component.jsx)
(http://localhost:9000)

When we click on the Increment button, the App component state changes and count increments by 1. When count reaches 3, the button gets disabled and we can no longer increment the counter. Since the initial value of count is 0, we are expecting the maximum 3 possible button clicks.

Now that we know what the App component can do, we can write a test case for it to test the above behavior. So let’s create app.test.js file in the app directory and write a test case for it.

Before we start working on the test case implementation, let’s understand how DOM works in Jest. Every test suit (each .test.js file) receives an instance of Document through the document global variable just like test and beforeEach global function. The document object gives us browser-like DOM API to work with. You can create, read, update, or delete DOM elements as well as listen or dispatch events on them.

Since each test suit gets its own document object, we do not have to worry about polluting DOM. However, if a test suit has multiple tests, a DOM modification done in the previous test would still persist in the next one. Hence we would need to clean DOM before the next test is executed.

(src/components/app/app.test.js)

As you can see from the above test implementation, first, we are appending a div element with id app to the <body> before running each test using the beforeEach block. Using the afterEach block, we are removing the same element so that the next test has a clean <body> to work with.

Right now, we haven’t done any rendering of the App component or assert its functionality. So let’s move on to that. First, we need to import render function from the react-dom package to render a React component in a DOM element. The unmountComponentAtNode function unmounts and cleans the rendered component in a specific DOM element.

The react-dom/test-utils package gives us some functions to prepare React components for the tests. The act function takes a callback containing the rendering logic of the React component. This function makes sure that the React component is properly mounted and ready for assertions.

(src/components/app/app.test.js)

In the above test case, we are first accessing the div element that was appended to the body in the beforeEach block. We are going to render the App component inside this div. This was done using the render function from inside the act function. Once the component is rendered, using the console.log statement, we are checking the innerHTML of the body.

From the above results, we can see the div with id app and all the HTML rendered by the App component. You can also see the Monica Geller text inside the HTML which was the name prop value. Using this HTML, we can assert if our App component is rendering the correct HTML.

You can use the native DOM API to read DOM elements and make assertions. For example, document.querySelector() method returns an HTML DOM element and you can use .innerHTML or .textContent properties of a DOM node to read its contents. But I would recommend you to use @testing-library/jest-dom package which extends the Jest’s expect API with matchers that can be used for DOM assertions. What you need to do is to add import '@testing-library/jest-dom'; import statement at the top of the test file. Let’s see this in action.

The .toHaveTextContent(content) matcher checks if a DOM element has the content string value. Let’s modify the above test and assert the value inside the h1 element which should be “Monica Geller”.

(src/components/app/app.test.js)

As you can see from the results above, the test has passed. Now let’s move on to the fun part. What we now have to assert is the text content of the <h2> element which has the “count” value. Initially, it will have the count: 0 text inside it because the count state of the App component is 0.

But when we click on the “Increment” button, this text content should change. But how do we trigger button click from the test? Well, we can programmatically trigger an event such as a mouse click using the element.dispatchEvent(event) method on a DOM element. The event argument is an event object such as a MouseEvent.

(src/components/app/app.test.js)

In the above example, we are rendering the App component and asserting the initial text content of the <h2> element which should be count: 0. Also, the “Increment” button should not be disabled initially.

Then we fire a mouse click event on the button which should change the text content of the <h2> element to count: 1. We are also expecting the button not to be disabled here. However, after two more mouse click events, we are expecting the button to be disabled and the text content of the <h2> element to be count: 3. We have made multiple assertions for this in a single test.

The {bubbles: true} argument to the MouseEvent constructor makes the event bubble up the DOM tree. This is necessary as React attaches all the event handlers to the document or root of the component. Read more.

Until now, we have barely scratched the surface of testing React components using Jest. We haven’t really tested a React component that fetches the data from an API. In such situations, we might need to fake the API request and use a mock response. Also, we haven’t tested a callback invocation in a React component. There is much to explore. Follow this documentation to know more about this.

Setting up GitHub Action

If you are not familiar with what GitHub Actions is, follow my article on the same. I have explained what GitHub Actions is and how to set it up in your GitHub repository. You will also find the explanation and semantics of the workflow file which triggers the job (testing).

In a nutshell, GitHub Actions is testing environments provided by GitHub and a workflow file contains YAML formatted configuration of tasks to be run when an event occurs in the repository such as the push event which occurs when new files are pushed to the repository.

(.github/workflows/unit-tests.yml)

In our repository, inside .github/workflows directory, we have the unit-tests.yml file which is a workflow file for GitHub Actions. It is designed to execute the npm run test command whenever push event occurs on the master branch of the repository.

Whenever the push event occurs, it runs the test job on an ubuntu container (server). This job first sets up Node.js v12 and clones the repository code in the working directory of the runner. Then it installs all NPM dependencies by executing the npm install command and finally, we run the npm run test command which executes Jest tests.

(source: github.com)

Once the workflow file is checked in and a push event occurs on the repository, GitHub Actions will start the test job mentioned in the unit-test.yml workflow file. If the job was successful, we will get the green tick mark as shown above. Else, you will see a red cross mark.

You can also see the entire workflows, jobs, and status of individual tasks from the Actions panel as shown above. If you see a green tick, means your React components are working fine as expected.

JsPoint

JavaScript and the Web

Uday Hiwarale

Written by

Software Engineer at kausa.ai / thatisuday.com ☯ github.com/thatisuday ☯ thatisuday@gmail.com

JsPoint

JsPoint

A collection of essential articles for JavaScript, WebAssembly, TypeScript, Node.js, Deno, and Web development in general.

Uday Hiwarale

Written by

Software Engineer at kausa.ai / thatisuday.com ☯ github.com/thatisuday ☯ thatisuday@gmail.com

JsPoint

JsPoint

A collection of essential articles for JavaScript, WebAssembly, TypeScript, Node.js, Deno, and Web development in general.

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.

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

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.

Get the Medium app