When testing a component, you basically proceed in the same way as for an ordinary unit test: you prepare the environment, you perform an operation, and check the result.
As with testing a function, you should try to think in terms of inputs and outputs rather than checking the internal structures of a component. With a function component like the BooksListItem component, which is responsible for displaying a single item in a list, this isn’t possible anyway because you have no access to internal structures of the component. The values you pass to a component are represented by the props. The actions can range from successful initialization to interaction with the rendered structure. Finally, the outputs consist of the rendered structure or a called callback function that you previously passed as a prop.
For class components in React, it’s also possible to access the internal structures in the form of the state. However, you should avoid this too. A better idea is to test the effects of a state change that are visible to the users.
If you’ve set up your application using Create React App, the tool will already have installed the React Testing Library for you. This library makes component testing easier by providing you with numerous helper functions. However, before you start the actual test, you need to make some minor adjustments to the component. As mentioned earlier, when you unit test a component, you test the visual effects of an action. When you render a component, you typically check to see if certain elements are present. To make it easier to locate them and make your tests more robust, you should add data-testid properties for the elements you want to locate. The code below shows the updated source code of the component:
import React from 'react';
import { Book } from './Book';
import { StarBorder, Star } from '@mui/icons-material';
type Props = {
book: Book;
onRate: (bookId: number, rating: number) => void;
};
const BooksListItem: React.FC<Props> = ({ book, onRate }) => {
return (
<tr>
<td data-testid="title">{book.title}</td>
<td data-testid="author">{book.author}</td>
<td data-testid="isbn">{book.isbn}</td>
<td>
{Array(5)
.fill('')
.map((value, index) => (
<button
key={index}
onClick={() => onRate(book.id, index + 1)}
data-testid="rating"
>
{book.rating < index + 1 ? (
<StarBorder data-testid="notRated" />
) : (
<Star data-testid="rated" />
)}
</button>
))}
</td>
</tr>
);
};
export default BooksListItem;
In the component, you add the data-testid property for each of the table cells that contain the title, author, and ISBN. You also add this property to the buttons and star icons that you can use for rating purposes. Having made these preparations, you can now add a new test suite for rendering to the BooksListItem.spec.tsx file. This listing shows what the associated code looks like:
import renderer from 'react-test-renderer';
import { render, screen } from '@testing-library/react';
import BooksListItem from './BooksListItem';
describe('BooksListItem', () => {
describe('Snapshots', () => {…});
describe('Rendering', () => {
it('should render correctly for a given dataset', () => {
const book = {
id: 2,
title: 'Clean Code',
author: 'Robert C. Martin',
isbn: '978-0132350884',
rating: 4,
};
render(
<table>
<tbody>
<BooksListItem book={book} onRate={() => {}} />
</tbody>
</table>
);
expect(screen.getByTestId('title')).
toHaveTextContent('Clean Code');
expect(screen.getByTestId('author')).toHaveTextContent(
'Robert C. Martin'
);
expect(screen.getByTestId('isbn')).toHaveTextContent( ↩
'978-0132350884');
expect(screen.getAllByTestId('rating')).toHaveLength(5);
expect(screen.getAllByTestId('rated')).toHaveLength(4);
expect(screen.getAllByTestId('notRated')).toHaveLength(1);
});
});
});
In the test, you first prepare the data record that you want to display by using the component. Then, you use the render function of the React Testing Library to render the component. At that point, you want to make sure that the BooksListItem component returns a tr element as the root element. If you render the component directly, you get a warning that a tr element must not occur in a div element. That’s because Jest represents the component structure in a div element. To work around this problem, you render the component in a combination of table and tbody elements.
After these two steps, the arrange step and the act step, it’s time to review the result. You can use the screen object to perform these checks. Alternatively, the render function also returns an object that contains, for example, the getByTestId method.
The getByTestId method returns a reference to the first element that has the specified data-testid property. With this reference, you can then use the toHaveTextContent matcher to check if, for example, the title is displayed correctly.
Using the getAllByTestId method, you get all elements with the respective data-testid property. This allows you to check, for example, whether the presentation of the rating is correct.
When you run your tests via the npm test command, you should get a success message that all tests have been run successfully.
The BooksListItem component is not only used to display a data record, but also has an interface that allows users to interact with the component. The five button elements can be used to evaluate the data record. Clicking on one of the button elements triggers the onRate function, which is passed to the component as a prop. You can also secure this aspect of a component by means of a unit test. For this purpose, you render your component, execute the action (i.e., the click), and then check whether a corresponding response (i.e., the call of the onRate function) has occurred. This listing contains the source code of the test:
import renderer from 'react-test-renderer';
import { fireEvent, render, screen } from '@testing-library/react';
import BooksListItem from './BooksListItem';
describe('BooksListItem', () => {
describe('Snapshots', () => {…});
describe('Rendering', () => {…});
describe('Rating', () => {
it('should call the onRate function correctly', () => {
const book = {
id: 2,
title: 'Clean Code',
author: 'Robert C. Martin',
isbn: '978-0132350884',
rating: 4,
};
const onRate = jest.fn();
render(
<table>
<tbody>
<BooksListItem book={book} onRate={onRate} />
</tbody>
</table>
);
fireEvent.click(screen.getAllByTestId('rating')[2]);
expect(onRate).toHaveBeenCalledWith(2, 3);
});
});
});
Compared to the previous test, you adapt the arrange step in such a way that you create a spy function in addition to the data record. You have already become familiar with the test doubles of Jest in the context of the mock function to replace the behavior of Math.random. Now the spy function that you create via jest.fn is simply used to check the interaction with the component. When the button is clicked on, the onRate function is indirectly called with certain parameters.
When rendering the component, you must again make sure that the structure is correct and that both the data record to be displayed and the onRate function are passed correctly. Then you use the click method of the fireEvent object to trigger a click event on the third rating button. This causes the onRate function to be called with the values 2 for the ID and 3 for the rating.
The “act” Function: If you research the topic of React and unit tests on the internet, sooner or later you’ll come across the act function. This feature allows you to encapsulate actions such as rendering a component or interactions such as a click. The act function makes your test behave similarly to the actual execution of React in the browser. If you use the React Testing Library, you no longer have to bother with using the act function because all of the library's helper functions use the act function internally, ensuring that your tests and the application behave in a similar way in the browser.
You can use the toHaveBeenCalledWith matcher in conjunction with the spy function to check whether the function was called correctly. When you run your tests with this state, you’ll see that all tests are executed successfully.
Editor’s note: This post has been adapted from a section of the book React: The Comprehensive Guide by Sebastian Springer. Sebastian is a JavaScript engineer at MaibornWolff. In addition to developing and designing both client-side and server-side JavaScript applications, he focuses on imparting knowledge. He inspires enthusiasm for professional development with JavaScript as a lecturer for JavaScript, a speaker at numerous conferences, and an author. Sebastian was previously a team leader at Mayflower GmbH, one of the premier web development agencies in Germany. He was responsible for project and team management, architecture, and customer care for companies such as Nintendo Europe and Siemens.
This post was originally published 4/2025.