🔬 React Testing - How to Test React Components?
Unit testing is an essential part of any software development process. It allows developers to test individual components of the application and catch any bugs before they make it into production. React.js, being one of the most popular front-end frameworks, has a lot of resources available for unit testing.
Why You Should Write Unit Tests
Unit testing is a crucial step in the software development process, where developers meticulously examine the smallest functional components, known as units, to ensure their proper functioning. This process involves thorough testing conducted by software developers, and occasionally by QA personnel, as an integral part of the development lifecycle. Unit testing helps us have faith and strong faith at that, that our software works well and in the most bizarre use cases. Sometimes, it's difficult to test and cover all use cases before the software is deployed, but unit testing helps us test different independent units of our software. Making sure that they are working correctly and will stand the test of time.
What Should You Test
As the name suggests we should be testing a unit, not more, not less. If it is a function we should be testing only that function and not its dependencies. If it's a react component it should only be that react component.
What to Test
In general, your tests should cover the following aspects of your code:
- If a component renders with or without props
- How a component renders with state changes
- How a component reacts to user interactions
What Not to Test
Testing most of your code is important, but here are some things you do not need to test:
- Actual Implementation: You do not need to test the actual implementation of a functionality. Just test if the component is behaving correctly.
- Third-Party libraries: If you are using any third-party libraries like Material UI, no need to test those – they should already be tried and tested.
Tools for React Unit Testing
React Testing Library
The React Testing Library has a set of packages that help you test UI components in a user-centric way. This means it tests based on how the user interacts with the various elements displayed on the page.
Vitest
Vitest is a next-generation testing framework powered by Vite. You can check out the Comparisons section on vitest website for more details on how Vitest differs from other similar tools.
How to Write Unit Tests
We are not going to deep dive into how to set up, configure and the file paths of tests. we’ll check the general idea and the strategy of writing unit tests. and try to talk through that.
Test Rendering Components
Let's write a component that renders "Hello World" and test that the component renders the popular greeting:
Now, we will create a test file:
In this test case, we render the HelloWorld
component using the render
function provided by React Testing Library, The render
function is used to render the component in a DOM, this is similar to the DOM in the browser. Once this is done we can then test the component using DOM API-like functions provided by the React Testing Library. We then use the getByText
function to retrieve the element that contains the "Hello World" text. Finally, we use the toBeInTheDocument matcher to check if the element is present in the rendered component.
We can assign a test-id
to elements in our component so we can pinpoint them directly by using the getTestById function provided by React Testing Library.
See the use of data-testid
property in the div element there. To get the div element, we will call the getByTestId
function passing in the value of the data-testid
to the function. This returns the HTMLElement instance of the div element and then we can test the Hello World text node using the textContent
DOM property.
Test Firing Events
Let's say we have a Counter application that updates the DOM with the click of a button:
Now, we have a state increment that holds the state of the application, and it is displayed in the DOM. The Increment button when clicked increases the state increment by one. So let's write a test for this component to make sure that the increment state is increased when the Increment button is clicked.
See that we got the DOM instance of the button by calling this getByText('Increment')
, then we called the click
method on its instance, this will fire the click event on the button causing the increment state to be increased by one, then we will thereafter to see if the state was really updated. See in the last line, we got the text node of the div element and expect it to be Increment: 1
.
State and Props of the Components
We have a state count
, its initial value is from props
and it is displayed in the DOM in the p
element. The Increment button when clicked increments the state of the count by 1 and this in turn makes the component re-render and displays the updated value of the count state.
Mocking Function Calls
During testing, we might not really want an actual function to be called based on some factors. For example, the function might have a number of calls set on it. The only way to go about this is to mock that function, ie, to create a dumb version function of that actual function.
We have a simple component here, it accepts a function in its props object via the done
property. This done
props function is called when the Call DONE button is clicked.
Testing React Hooks
Now, let's write a test case for this custom hook:
Testing Asynchronous Operations
Let's see a component that on render makes an HTTP call to an endpoint and then renders the result of the fetch.
This component fetches data from https://api.example.com/data and renders it. Now, we want to test this component but we don't want the component to make an actual HTTP call to the endpoint. We will mock the fetch call.
You can also use waitForNextUpdate
here for data fetching.
Snapshot Testing
Snapshot testing is quite different from what we have seen above. This type of testing is classified as output comparison testing. In the case of React component snapshot testing, the UI of the component is taken first and saved, then on subsequent testing, a current snapshot of the component is taken and compared with the previous snapshot to check for changes that may cause breaks.
To create a snapshot test for this component, you can write a test case using the toMatchSnapshot
matcher:
If you intentionally make changes to the component's output and want to update the snapshot, you can update the snapshot file using the following command:
Summary
In summary, I’d say the most important thing is writing testable code. That allows you to write better unit tests and write them fast. Key facts in writing testable code are:
- Separation of UI and logic
- Passing dependencies to function (or dependency injection)
- Make sure to test a single unit of code