Testing is an essential part of the software development lifecycle, helping to identify and address issues early in the development process, reduce the risk of defects in production, and ultimately deliver a higher-quality and more reliable software product.
The basic idea behind implementing testing on your application is to ensure it works as intended. However, the broader idea behind conducting tests on your React application is to make it less prone to errors and deliver a good user experience. Furthermore, applications are typically updated frequently; testing ensures the application doesn’t break and the code is still reliable.
Types of Testing
Three types of tests can be written for React applications:
- Unit tests: These types of tests are performed to test the functionality of an individual component in isolation.
- Integration tests: These tests ensure the proper functioning of different components working together. It tests the interaction between the parent and child components or when the components contain some dependencies.
- E2E tests: These tests ensure a proper user experience is received from the user perspective by testing an entire application.
Writing Unit Tests for React Components
Unit tests if performed to test an individual component in isolation. The idea behind implementing unit tests for React components is to ensure each component works as intended regarding its design specifications.
The anatomy of unit tests follows the AAA approach, which stands for:
- Arrange: In this section, as the name suggests, prepare the system for testing. It involves configuring the necessary dependencies and creating required objects.
- Act: In this section, you operate on the system that is under test. It may involve calling a function.
- Assert:In this part, we ensure the outcome matches the expected result. It involves making assertions on the system’s behavior that is under test to ensure its outcome.
React Testing Components with Vitest and React Testing Library
Here we are focusing on automated testing, to add testing to your project you need 2 things:
- A test runner which acts as a framework where we can run tests.
- A testing library that enables testing different areas of our application.
For our purposes we are using Vitest as our test runner because we use Vite as our build tool and Vitest is a Vite native test runner so it integrates nicely with Vite, and React Testing Library as our testing library as it provides light utility functions on top of react-dom and react-dom/test-utils. To install these tools to our project, run the following code in your terminal.
For the purpose of testing react components in node.js with the many web standards, we will also need jsdom because we will be running tests in the node environment. But we’re testing browser interactions through the DOM.
npm install –save-dev jsdom @testing-library/jest-dom
The @testing-library/jest-dom library is required because it contains assertions like toBeInTheDocument, toHaveBeenCalled, and others which make it easy to test for DOM elements.
One of the main advantages of Vitest is its unified configuration with Vite. If present, vitest will read your root vite.config.ts to match with the plugins and setup as your Vite app. For example, your Vite resolve.alias and plugins configuration will work out-of-the-box. You’ll also need to add a reference to Vitest types using a triple slash directive at the top of your config file.
Command Line Interface
In a project where Vitest is installed, you can use the vitest binary in your npm scripts, or run it directly with npx vitest. Here are the default npm scripts in a scaffolded Vitest project:
As an example, we will write a simple test that verifies the output of a function that adds two numbers.
Then, run npm run test,and Vitest will print this message:
Now with our test runner setup, to do any meaningful testing in React we need to test React Components.
The actual tests are typically written in a .test/spec file. When the test file contains multiple test cases they can be grouped within a describe function to organize them better.
Also when testing with components we will also need to simulate user events, react testing library has a built in fireEvent function but to better simulate actual user interactions we will use the testing library user events module.
npm install –save-dev @testing-library/user-event
When using the react testing library, most of the time we make use of two functions from the library, render and screen.
Here render is used to actually render the component that we want to test in a test environment, and screen is used to query the rendered component and get specific elements to test them.
In the case below:
We first arrange the test so it renders our TestComponent, then we act on the rendered result so that the rendered output is correct.
RTL gives us the query method to find elements on the rendered page. Through queries we select the page content that we want to act upon.
Queries are mainly of 2 broad types:
- Single Elements
- Multiple Elements
getBy…: returns matching node for a query, and throw a descriptive error if no elements match or if more than one match is found (use getAllBy for multiple)
queryBy…: Returns the matching node for a query, and returns null if no elements match. This is useful for asserting an element that is not present. Throws an error if more than one match is found (use queryAllBy for multiple)
findBy…: Returns a Promise which resolves when an element is found which matches the given query. The promise is rejected if no element is found or if more than one element is found after a default timeout of 1000ms (use findAllBy for multiple)
So we can select our required element in the page by these queries then act upon them to check if they are rendered properly, and assert the presence of required elements in the page. We can also use these elements for interactions with the UI such as clicking a button and then we can assert that the desired result was achieved with the expect function.
We can also test the working of a function through mock functions. We make use of a mock function and pass it to the component whose function we need to test. For eg:
For practical implementation when testing, we need to mock the dependencies used in the project, in our case we are using React Router, Chakra Ui, Apollo Client thus we need to pass these dependencies that our actual component needs to properly render as a wrapper to the render function.
By doing so we can simulate the necessary data providers that our component needs to run. Hence by doing so we can test that different components are working together and how they interact with each other.
When testing we also need to test integration with API, so we can mock the API call so that it works well with our test.
Here we have made a custom render function for our tests that has a wrapper with the mock API providers.Thus we can use the mock API that our component needs to run successfully.
Our App component is using our custom render so that it has access to the required API through the wrapper provided by the custom render.
Now we can check if our signIn functionality is working as so:
The software development process includes React testing as a crucial step. This detailed tutorial demonstrated how to perform React testing using a variety of libraries, including Jest, Enzyme, and Cypress. These frameworks crucially aid unit tests, integration tests, and E2E testing of React components.
No matter which React testing library is chosen, it is essential to test under real user conditions.
Testing your React application before releasing it to real-world users has several advantages, including ensuring that all users have a positive experience with it.