Practical Approach to Unit Testing in React using Jest
A step by step procedure to testing react applications using Jest
A large number of companies that hire developers are concerned about creating error-free and frictionless applications. And one key strategy to accomplishing this is to incorporate some type of testing inside the application, which might help to test various components of the application before releasing it to the public. Testing has progressed through many levels over the years. We have numerous steps of testing that help to confirm an application's legitimacy before it is deployed.
The practical approach to unit testing in a React application will be covered in this article. We will use the built-in React testing library as well as Jest, a sophisticated and widely used testing framework. We chose Jest because of its easy-to-ride-on capabilities when developing a react application. For the article, we will create a small React application that covers certain major functionalities and write some tests for these aspects.
Prerequisite
Knowledge of HTML, CSS and Javascript
Knowledge of React
Very basic knowledge of Unit Testing with JEST
Project Setup
To integrate the use of Jest in our application, we need to bootstrap our react application by creating the application first. We will be using the “create-react-app” command to set up the project. Now let’s get straight into creating our project. Open up a new terminal and type in the command: “npx create-react-app react-jest”. This will automatically create a new project directory with the name: “react-jest”. Having done that, you should have a similar project structure to the output below:
You can see in the image above that various files and folders were generated automatically within the react-jest project directory. We will now manually create two extra directories within the src folder. Create a "components" and "__tests__" folder in the "src" folder. We won't spend as much time on the react side of things because our main focus is testing react applications with Jest. If you have created the folders, your project structure should look like this:
Let's go over what each of these files will do. The components folder contains components that are reusable throughout our program. As previously stated, several designs and functionality in a react application that appear to be reusable across multiple places are saved in the components folder. The "__tests__" folder, which may appear or sound new and surprising to most people, is used to store all test files for each component we want to test in our program.
For the sake of our testing, we will be creating a very simple application that displays a list of users which are retrieved from a third-party API. In that case, we will be doing some testing for this feature and the components we will be creating in general. Before writing our tests or creating the component, What exactly is Jest?
Introduction to Jest and the testing-library package
Jest is a tool for testing user features in an application. The objective of using jest for testing is to simulate a user's behaviour within an application and test for conceivable situations that could result in issues when a user interacts with an application. A variety of different tools, libraries, and frameworks could also be used for testing. We have Mocha, Enzyme, and so on. Jest appears to be widely used and simple to integrate into a react codebase. In addition, when developing tests in our application, we use the "testing-library/react" package, which is installed when you bootstrap your project. It enables you to test your application in a user-centered manner.
Code Setup
We have been able to explain why we need Jest and what it is all about. Now let's get our project running. Find the "App.css" file and delete the file because we will not be doing any form of styling in this project. Also, go into the App.js file and remove all codes within the return block. Copy the code below and paste it inside the App.js file:
import { useState } from 'react';
function App() {
const [isClicked, setIsClicked] = useState(false);
const handleGreeting = () => {
setIsClicked(true);
}
return (
<div className="App">
<h3>React App with Jest</h3>
{
isClicked && <h2>Good evening Sir/Madam</h2>
}
<button onClick={handleGreeting}>Click to greet</button>
</div>
);
}
export default App;
The code above is used to display a greeting when the button is clicked. The output of this code will be the exact output as what is below
And when you click the button, the output on your browser will be changed to the result below
Let's go a little further in our development now. Within the components folder, we will create a new component. This component will be called List, and it will be used to display the list of people that we obtained through an API. Create a new file called "List.js" in the components folder you created. Copy and paste the following code into the newly created file
import { useEffect, useState } from 'react';
export default function List() {
const [users, setUsers] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(false);
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/users')
.then((response) => response.json())
.then((data) => {
setIsLoading(false);
setUsers(data);
})
.catch((err) => {
setIsLoading(false);
setError(true);
});
});
const viewUser = (id) => {
fetch(`https://jsonplaceholder.typicode.com/users/${id}`)
.then((response) => response.json())
.then((data) => {
console.log(data);
})
.catch((err) => {
console.log(err);
});
}
if (isLoading) {
return (
<h4 data-testid="list-component">Users data loading...</h4>
)
}
return (
<div data-testid="list-component">
<h2>Users List</h2>
{
error ? <h4>Unable to fetch Users</h4> :
<table>
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Username</th>
<th>Phone Number</th>
<th>Website</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{
users.map((user) => {
return (
<tr key={user.id}>
<td>{ user.name }</td>
<td>{ user.email }</td>
<td>{ user.username }</td>
<td>{ user.phone }</td>
<td>{ user.website }</td>
<td>
<button onClick={() => viewUser(user.id)}>View details</button>
</td>
</tr>
)
})
}
</tbody>
</table>
}
</div>
)
}
A summary of what this component does is display a list of users that have been collected via a third-party API. The component has a loading state that informs the user that the user's data is being retrieved. It also features a loading completed stage, which shows the retrieved details, and an error state, which shows the error if the data retrieval was halted.
This component will be imported into the "App.js" component to be seen on your browser webpage. Import the component and declare it inside the template section to update your "App.js" code. You can change your App.js code to the following:
import { useState } from 'react';
import List from './components/List';
function App() {
const [isClicked, setIsClicked] = useState(false);
const handleGreeting = () => {
setIsClicked(true);
}
return (
<div className="App">
<h3>React App with Jest</h3>
{
isClicked && <h2>Good evening Sir/Madam</h2>
}
<button onClick={handleGreeting}>Click to greet</button>
<div>
<List />
</div>
</div>
);
}
export default App;
Having done that, The output on your webpage will now be exactly as in the image below
As said earlier, we will not be dwelling on styling so as not to get carried away with the actual context of this article. Now, we have a lot of features to test for. We’ve got two (2) components to test for and also several test cases within these two components. To have a clearer picture, we will be testing for the following in the two components
First component: (App.js)
Test to confirm that the component is rendered correctly
Test to confirm the display of the page title
Test to confirm the display of the greeting when the button is clicked
Test to confirm the List component is imported into the App.js file.
The second component (List.js)
Test to confirm the component is rendered
Test for API call and display of the user details on the page
Test to confirm the API call on click of the view details button on each user data in the table.
Now that we have a clearer picture of the list of things we would be testing for, let’s get started with the first on the list
Writing our first test
To get started with this, look out for the App.test.js file which is inside of the src folder, and move it directly inside of the test folder that was created earlier. All your tests will be placed inside the test folder that you created. Now remove every code inside of the file and leave the file empty. Our first test in the App.js file will be to test if the component is rendered successfully. Before doing that, there are some key concepts and structures in Jest that you need to be familiar with. And the reason is that we will be making use of it often while diving deeper into more test cases. Let’s look at the following keywords and inbuilt methods:
- The test function: To write a test for a particular feature, you need to declare a test function and this accepts a string as its first parameter denoting what exactly you are testing for. The syntax for this is
test('a new test', () => {});
It is used to declare a new test and the word wrapped in a string shows what the test block is doing.
- The Render function: is used to render a component directly in the test file. In the process of getting a component tested, the component will be imported into the test file and also rendered in the test block. The sample syntax for this is
import App from "../components/App";
test('renders component', () => {
render(<App />);
});
- The expect function: This is used to assert a certain condition that needs to be met when writing a specific test case. It is used inside of the test code block every time you need to test a value. The sample syntax is
import App from "../components/App";
test('renders component', () => {
expect(condition to be met).toBeInTheDocument()
});
We have clearly explained some concepts we will be using in this article going forward, let’s now go into writing a test case for the first component (App.js). The first test case we will be writing is to test if the component is rendered correctly. To do that, go into the App.test.js file located inside the tests folder and paste the following code inside of it
import { render } from '@testing-library/react';
import App from '../App';
test('renders component', () => {
render(<App />);
});
This is the first test we'll write for our App component, and it's just a simple check to see if the component is rendered appropriately. After that, we need to run the test command, which now runs the test and returns a status indicating whether or not it passed. If you look in your project's "package.json" file, you'll notice a test script in the scripts section.json
The test command will be used to execute the tests that we will write. To run the first test, we'll type "npm run test" into our open terminal. The terminal should be used to navigate to the project directory where you are presently working. Your test should produce the following results
Congratulations! We were able to complete our first test. Before moving on to other test cases, I'd like to reiterate several points that were previously discussed. The render function and the test function/code block are both used. These functions are directly imported from the "@testing-library/react" package. Now that we've written our first test case, let's go a little further and build some more relevant test case scenarios. We shall analyze the following scenarios:
In the App.js component, a test case successfully renders the page title.
When the button is clicked, this test case displays the greeting.
We have outlined the next set of cases we want to write, Let’s get into it. For the first on the list, update the test file to what we have below:
import { render, screen } from '@testing-library/react';
import App from '../App';
test('renders component', () => {
render(<App />);
});
test('renders page title', () => {
render(<App />);
const headerElement = screen.getByText(/React App with Jest/i);
expect(headerElement).toBeInTheDocument();
});
Looking at the code above, the new test added checks if the page renders the title. It first renders the component (App). it further checks for the text using the screen inbuilt method in the react testing package and uses regex expression to check for the matching word and finally, it uses the expect function to assert that the element found should be present in the DOM.
Let’s write the second test so we can run both of them at the same time. For the second test, we need to add the fireEvent as a new imported method in the testing library react package. This means the import line in our test file needs to be updated to the code below
import { render, screen, fireEvent } from '@testing-library/react';
Then our next test will be the code below
test('display greeting when button is clicked', () => {
render(<App />);
fireEvent.click(screen.getByText('Click to greet'), () => {
expect(screen.getByText('Good evening Sir/Madam')).toBeInTheDocument();
});
});
This newly added test renders the component (App), Then uses the fireEvent method in the testing library to fire a click event. The fireEvent acts as a button in the DOM. Then in its callback, it checks for the text displayed on the page using the screen.getByText method and finally uses the "expect" method to assert the expected behaviour.
Now we have both tests written, rerun the terminal now and you should get the result below:
According to the results above, our three (3) tests written thus far have all passed satisfactorily. This is why the terminal shows three passes, three total. So far, we have written three tests for our App.js component, and we will be adding more. This will be used to see if the List component was appropriately loaded into the App.js file. To accomplish this, we will insert a new test block into the test file. This new block will handle testing for our use case. Go to the file and insert the following code
test('Check if list component is present in App component', () => {
const { getByTestId } = render(<App />);
const listComponent = getByTestId('list-component');
expect(listComponent).toBeInTheDocument();
});
You will notice that the new test block has some similarities with other tests. The only difference now is we are introducing a new query method which is the “getTestById”. This query type is used to find an element in the component/DOM that has an attribute of data-testid in it. For example:
<div data-testid=”test-component”>
<h2>My test component</h2>
</div>
In our scenario, we want an element with its data-testid attribute value as a list-component. If you recall, we had a list component (List.js) that contained various elements, one of which contained the data-testid that we were seeking. Because this component was imported into the App.js file, we can access its data-testid attribute from the App component. We can now rerun our terminal to see the status of all our previous tests. We'll use the same "npm run test" command, and the results of our test should look like the one below:
So far, we have been able to write four (4) different test cases for our App.js file and we successfully implemented the 4 because they are all passed test cases. Before we go into writing tests for the second component which we have, you might want to check out some of the query methods which we used in the test file. To have a deeper understanding of the several methods of the testing library, You can check out the official documentation.
We’ve been able to explore a lot of test cases and methods by just testing the first component in our application. Let’s get into writing some tests for the second component which is the List.js file. We know the second component to be the component that makes the API call and displays the list of users retrieved from the API call, it also allows you as a user to view details of a particular user when the view details button for that user is clicked. We will be testing for the following cases:
Test that the page is rendered correctly.
A test for the API call that was made on useEffect.
A test for the API to get the value of a user on click of the view details button.
The lists above are the lists of tests we will be writing for our second component. The very first on the list is super easy because we’ve done similar stuff in the first set of tests we wrote. Now, we will need to create a new file inside of the “__tests__” folder and name it “List.test.js''. This file will contain all our tests for the List component. If you have created the file, now paste the first test block inside of it
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import List from "../components/List";
test('renders component', () => {
render(<List />);
});
Right now, we have imported some functions from the test library which we aren’t using at the moment. Like the fireEvent, screen, and waitFor. We will be using them on other tests that we will write. If we run the test command in the terminal again, our output will be:
Looking at the result of the test above you would notice a change in the terminal. We now have 2 test suites in total which mean we’ve got 2 files we are testing for, And we also have 5 passed tests in total which mean we’ve got 4 tests passed from the first test file and the new test we’ve just added. Going further, we will write the test for the next test case which is the test for making the API call immediately after the page gets loaded. Now paste the code below into your List.test.js file:
test('api call immediately page gets loaded', async () => {
const fakeUsers = [
{
id: 1,
name: 'John Doe',
email: 'john@gmail.com',
username: 'Johnny',
phone: '+2349098812189',
website: 'john.xyz.com',
},
{
id: 2,
name: 'Alexa Graham',
email: 'alexis@gmail.com',
username: 'Alexa12',
phone: '+2349987129189',
website: 'alexis.brr.com',
}
]
// Mock the API call and return the fake users'
jest.spyOn(global, 'fetch').mockResolvedValue({
json: jest.fn().mockResolvedValue(fakeUsers),
});
render(<List />);
// Wait for the loading text to disappear
await waitFor(() =>
expect(screen.queryByText('Users data loading...')).not.toBeInTheDocument()
);
// Check if the users are rendered correctly
expect(screen.getByText('John Doe')).toBeInTheDocument();
expect(screen.getByText('Alexa Graham')).toBeInTheDocument();
// Restore the original fetch implementation
global.fetch.mockRestore();
});
The comments in this code block explain what each block performs in detail. First, we mimic our API by generating mock data, and then we used the Jest global fetch method to replicate the fetch api method used in the component/DOM. After that, we'll proceed to render the component. At this time, the component does not display the data; instead, it displays a loading state that informs the user that the API call is currently being performed. Because we are displaying the words 'Users data loading...' in our component, we are using the waitFor() function in our test to simulate that behaviour. We use the "queryByText" inside the waitFor to ensure that the loading state is no longer in the DOM before displaying the fake users. Finally, we state some requirements that must be met, including the fact that we expect users to be in the DOM. We also revert to the earlier fetch implementation. If we perform our test now, we will obtain the following result:
We will be reusing some elements of the recently completed test for our final test, which will result in duplication of the same code in our test file. So, before we create our last test, we'll make some changes. We'll go over how to use the Describe function. Describe is a function that encapsulates a collection of related tests. In this manner, we can use the beforeEach and afterEach functions to separate the code that will be called before and after each of the test blocks. Now that we've discussed it, change the file to the code below
describe('API related test', () => {
beforeEach(async () => {
const fakeUsers = [
{
id: 1,
name: 'John Doe',
email: 'john@gmail.com',
username: 'Johnny',
phone: '+2349098812189',
website: 'john.xyz.com',
},
{
id: 2,
name: 'Alexa Graham',
email: 'alexis@gmail.com',
username: 'Alexa12',
phone: '+2349987129189',
website: 'alexis.brr.com',
}
]
// Mock the API call and return the fake users
jest.spyOn(global, 'fetch').mockResolvedValue({
json: jest.fn().mockResolvedValue(fakeUsers),
});
render(<List />);
// Wait for the loading text to disappear
await waitFor(() =>
expect(screen.queryByText('Users data loading...')).not.toBeInTheDocument()
);
});
afterEach(() => {
// Restore the original fetch implementation
global.fetch.mockRestore();
});
test('api call immediately page gets loaded', () => {
// Check if the users are rendered correctly
expect(screen.getByText('John Doe')).toBeInTheDocument();
expect(screen.getByText('Alexa Graham')).toBeInTheDocument();
});
});
You'll see that we've changed our test by transferring some significant chunks of it into the beforeEach method and also moving certain pieces to the afterEach function. This implies that our test will execute the code inside the beforeEach block before running the code inside the test block, and it will also run the code inside the afterEach block immediately after running the code inside the test block. This way, if we have numerous tests that require the same code, we won't have to copy and paste it directly into the test blocks. We can now proceed to write our final test. The code for our most recent test is as follows:
test('test for api call on click of view details button in table', () => {
const userDetail = {
email: "Sincere@april.biz",
id: 1,
name: "Leanne Graham",
phone: "1-770-736-8031 x56442",
username: "Bret",
website: "hildegard.org",
}
jest.spyOn(global, 'fetch').mockResolvedValue({
json: jest.fn().mockResolvedValueOnce(userDetail),
});
const viewDetailsButton = screen.getAllByRole('button', { name: /View details/i });
viewDetailsButton.forEach((button) => {
fireEvent.click(button);
});
});
This code should be inserted within the describe block. Just below the end line of the first test within the block. For this final block, the codes in the beforeEach will be executed first, followed by the codes visible inside the test block, and finally, the code in the afterEach will be executed instantly. In the final test, we make another API request because it is the same thing we do in the DOM. Then we moved on to look for the view details button and triggered a click event on it. Now that we've covered everything, let's run our test and see what happens. If you rerun the test, you should get the result below:
Congratulations if you got this far. It’s been a long ride on the article but all areas we have explained were needed because we are looking at a practical approach as the topic says.
Conclusion
We have been able to visit a lot of areas and use cases where testing was needed within our sample application. One thing you should know is that testing is way deeper than all that we have covered. It wouldn’t be justifiable to say we have covered all about testing in just one article but what we have implemented so far would give you a level of confidence you need to write unit testing with Jest in your react application. If you need the exact codebase for this article, You can find it in my GitHub repository.
References
Support
If you found this article useful and interesting, please leave comments, likes, and additions to anything that was supposed to be included but was omitted. Every day, we all learn something new, and I am always willing to learn anything new from anyone, at any time. Please follow me and stay tuned for more postings on various Frontend and Backend elements of software engineering. Do you require a continuation of an advanced technique to work with Jest in React? I'd be waiting in the comments for your YES.