In the realm of test automation, unit testing exercises a fragment of the application in complete isolation – comparing its actual behavior to the expected one. You don’t typically connect with external dependencies like databases, file systems, and HTTP services. This helps unit tests run faster and offer greater stability.
Developers can get immediate feedback on any changes to the codebase, and this saves last-minute hassles.
While testing React applications, our assertions are defined by how the applications render and respond to the user interaction. The ability to test React components in an easy and convenient way can help you create top-of-the-line front-end applications that customers seek.
In this article, we will discuss a step-by-step approach to testing React components using Jest and React Testing Library.
Jest is an open source JavaScript testing framework developed by Meta. It can verify any code written in JavaScript. It is widely used with React based applications and is great for unit testing. As a test runner, it is used for creating, running, and structuring tests. It offers functions for test suites, test cases, and assertions. It also help in determining whether a test successfully passes or fails.
The React Testing Library (RTL) provides a light weight solution to test React components. It is built on top of the DOM (Document Object Model) Testing Library. Unlike Jest, RTL is not a test runner or a framework. It can work out of the box for testing on web browsers that integrate with module bundlers like Webpack.
RTL provides a virtual DOM for testing React components. While running tests without a web browser, we need a virtual DOM to render apps, interact with the elements, and observe if the virtual DOM behaves as expected.
Let us begin by installing the required libraries and setting up the project.
npm install --save -D jest jest-junit @types/jest
npm install --save -D @testing-library/jest-dom @testing-library/react
Let us define the Jest configuration in the package.json
file.
Use 'test': 'jest --coverage'
within the script
tag.
The suiteName
is the project name, the outputName
is the target XML file, and the outputDirectory
is the folder name.
The public folder structure looks like the following:
Jest has a feature called coverage report that allow us to check if our code covers all lines of the files we choose by generating an HTML file that we can open.
Once we navigate to the coverage/lcov-report
directory there is an index.html
file that can be loaded into a browser. It includes the information printed at the command line along with additional information and some graphical output.
We can get the coverage once the tests are executed, and it should be available in the public folder.
Now, for all the Jest configuration let us create a jest.config.js
file.
module.exports = {
// All imported modules in your tests should be mocked automatically
automock: false,
displayName: {name: 'polygon', colot: 'blue'},
reporters: ['default', 'jest-junit'],
// The directory where Jest should output its coverage files
coverageDirectory: 'target',
moduleNameMapper: {
'\\.(css|scss)$': 'identity-obj-proxy',
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
'
}
// A list of paths to directories that Jest should use to search for files
roots: ['
// The path to a module that runs code to configure or setup the test framework before each test
setupFilesAfterEnv: ['./test/setupTests.ts'],
// The glob patterns that Jest uses to detect test files
testMatch: ['**/?(test|spec).(js|jsx|ts|tsx)'],
// An array of regex pattern strings that are matched against all test paths, matched tests are skipped
testPathIgnorePatterns: [
'/node_modules/',
'/scripts/',
'/dist/',
'/cypress/',
'/.cache/',
'.*\\.visual\\.(spec|test)\\.js'
],
// This option sets the URL for the jsdom environment
testURL: 'http://localhost/',
transform: {
'^.+\\.(js|jsx|ts|tsx)?$': 'babel-jest',
}
// An array of regex pattern strings that are matched against all source file paths, matched files will skip transformation
transformIgnorePatterns: [
'
]
verbose: true,
// Coverage threshold is how much are we covering mandatorily in the files
coverageThreshold: {
global: {
branches: 40,
functions: 50,
lines: 1,
statements: 1,
}
}
We will create a setupTest.js
to a module that runs code to configure or setup the test framework before each test.
import '@testing-library/jest-dom/extend-expect';
Unit tests can evaluate individual components in isolation. A unit can be a function, routine, method, module, or an object. The test determines whether the unit outputs the expected result for any given input.
A test module comprises a series of methods provided by Jest that describes the structure of the test. We can use methods like describe
and test
as shown below.
describe('my function or component', () => {
test('does the following', () => {
// Magic happens here
});
});
The describe
block is the test suite, while the test
block is the test case. A test suite can comprise multiple test cases. A test case need not be a part of any test suite, although it is widely practiced.
We can write assertions within a test case (for example, expect
in Jest) that validates Success or Error. Each test can comprise multiple assertions.
The following is a trivial example of assertions that turns out successful.
describe('true is truthy and false is falsy', () => {
test('true is truthy', () => {
expect(true).toBe(true);
});
test('false is falsy', () => {
expect(false).toBe(false);
});
});
Now, let us write our first test case to test the function TagList
from the TagList
component.
For this we must create a new file taglist.test.jsx
. Note that all the test files follow the pattern {file}.test.jsx
where {file}
is the name of the module file to be tested.
We have used the act()
function to ensure that all updates like rendering, data fetching, or user events have been processed and applied to the DOM before you create any assertions.
Please refer to the Testing Recipes – React (reactjs.org) for more information on act()
We have also used the render()
function to render an individual component in isolation. The function will render on the initial load. The test fails only if there is a compilation error or an error in the function component that hinders its rendering.
We have passed the Test ID label-text
under the div component.
import {TagList} from '@client/components/TagList';
import {act, render, screen} from '@testing-library/react';
import React from 'react';
describe('Tag list entry page test case', () => {
it('When tag is simple on initial render', () => {
act(() => {
render(
items={[]}
label={''}
filterKeyName={''}
onCloseClick={jest.fn()}
/>,
);
});
expect(screen.getByTestId('label-text')).toBeDefined();
});
});
By describing the module and the test case, we get a clear idea on what went wrong if a test fails.
Now that our test is ready, we can execute it and verify the output using the following command:
npm run test
Let us focus on running a single test by using the following command:
npm test -- src/client/pages/PolygonFilter/__tests__/TagList.test.tsx
Observe that two tests were skipped and one passed. This is exactly how we wanted it to behave. But what would happen if something goes wrong? Let us add a new test to the TagList
test suite to find out.
it.only('When tag is static on render', () => {
act(() => {
render(
items={[]}
label={'Static'}
filterKeyName={''}
onCloseClick={jest.fn()}
/>,
);
});
expect(screen.getByTestId('tag-id')).toBeDefined();
});
As expected the test failed, and we received the information regarding the expect()
function call that failed, the expected value, and the actual outcome. Since we could detect an issue with our original function we can fix it as well.
it.only('When tag is static on render', () => {
act(() => {
render(
items={[{id: '1', name: 'static-text'}]}
label={'Static'}
filterKeyName={''}
onCloseClick={jest.fn()}
/>,
);
});
expect(screen.getByTestId('tag-id')).toBeDefined();
});
Now let us insert the items and execute the test again.
Our next step is to validate how the component reacts to the data collected from the API. But how can we test the data if we are not sure what the API’s response would be? The solution to this problem is mocking.
The purpose of mocking is to isolate the code tested from external dependencies such as API calls. This is achieved by replacing dependencies with controlled objects that simulate those dependencies.
Mocking is a three-step process:
Step 1: Import the dependencies
import{
useGetBusinessChainList,
useGetFilters,
} from '../../PolygonList/graphql/queries'
Step 2: Mock the dependency
jest.mock('../../PolygonList/graphql/queries', () => {
return {
useGetFilters: jest.fn(() => {
return { isloading: false, data: [] };
}),
useGetBusinessChainList: jest.fn((categoryId = '0')) => {
return { isloading: false, data: [] }
}),
};
});
Step 3: Fake the function outputs
useGetFilters.mockImplementation(() => {
return {
isLoading: True,
data: [],
};
});
Let us see them now in action.
it('When API is callig the loader is running', () => {
useGetFilters.mockImplementation(() => {
return {
isLoading: true,
date: [],
};
});
act(() => {
render(
countries={countries}
appliedFilters={appliedFilters}
onFormSubmit={jest.fn()}
setFilterConfig={jest.fn()}
setFilterDrawer={jest.fn()}
filterConfig={filterConfig}
/>,
);
});
expect(InlineLoader).toBeCalled();
});
Let us execute all the tests and observe the results.
Using React is a great way to build high-quality apps in contemporary times. With Jest and RTL, it gets easier than ever to test your React components and applications and ship products faster.
If you're a React developer, how do you test your components and apps? Do you use any other libraries and frameworks? If you have worked with Jest and RTL how has your experience been?
Sign up with your email address to receive news and updates from InMobi Technology